Skip to main content

claude_code_transcripts/
types.rs

1use serde::{Deserialize, Serialize};
2use serde_json::Value;
3
4// ---------------------------------------------------------------------------
5// Top-level Entry — one per JSONL line
6// ---------------------------------------------------------------------------
7
8#[derive(Debug, Clone, Serialize, Deserialize)]
9#[serde(tag = "type")]
10pub enum Entry {
11    // ── Message-bearing entries ──────────────────────────────────────────
12    #[serde(rename = "user")]
13    User(UserEntry),
14
15    #[serde(rename = "assistant")]
16    Assistant(AssistantEntry),
17
18    #[serde(rename = "system")]
19    System(SystemEntry),
20
21    #[serde(rename = "attachment")]
22    Attachment(AttachmentEntry),
23
24    #[serde(rename = "progress")]
25    Progress(ProgressEntry),
26
27    // ── Metadata-only entries (no envelope) ─────────────────────────────
28    #[serde(rename = "permission-mode")]
29    PermissionMode(PermissionModeEntry),
30
31    #[serde(rename = "last-prompt")]
32    LastPrompt(LastPromptEntry),
33
34    #[serde(rename = "ai-title")]
35    AiTitle(AiTitleEntry),
36
37    #[serde(rename = "custom-title")]
38    CustomTitle(CustomTitleEntry),
39
40    #[serde(rename = "agent-name")]
41    AgentName(AgentNameEntry),
42
43    #[serde(rename = "agent-color")]
44    AgentColor(AgentColorEntry),
45
46    #[serde(rename = "agent-setting")]
47    AgentSetting(AgentSettingEntry),
48
49    #[serde(rename = "tag")]
50    Tag(TagEntry),
51
52    #[serde(rename = "summary")]
53    Summary(SummaryEntry),
54
55    #[serde(rename = "task-summary")]
56    TaskSummary(TaskSummaryEntry),
57
58    #[serde(rename = "pr-link")]
59    PrLink(PrLinkEntry),
60
61    #[serde(rename = "mode")]
62    Mode(ModeEntry),
63
64    #[serde(rename = "worktree-state")]
65    WorktreeState(WorktreeStateEntry),
66
67    #[serde(rename = "content-replacement")]
68    ContentReplacement(ContentReplacementEntry),
69
70    #[serde(rename = "file-history-snapshot")]
71    FileHistorySnapshot(FileHistorySnapshotEntry),
72
73    #[serde(rename = "attribution-snapshot")]
74    AttributionSnapshot(AttributionSnapshotEntry),
75
76    #[serde(rename = "queue-operation")]
77    QueueOperation(QueueOperationEntry),
78
79    #[serde(rename = "marble-origami-commit")]
80    ContextCollapseCommit(ContextCollapseCommitEntry),
81
82    #[serde(rename = "marble-origami-snapshot")]
83    ContextCollapseSnapshot(ContextCollapseSnapshotEntry),
84
85    #[serde(rename = "speculation-accept")]
86    SpeculationAccept(SpeculationAcceptEntry),
87
88    /// Catch-all for entry types not yet recognised by the ingest binary.
89    /// Allows forward-compatible parsing: new Claude Code entry types in
90    /// the JSONL will be silently skipped rather than aborting ingest.
91    #[serde(other)]
92    Unknown,
93}
94
95// ---------------------------------------------------------------------------
96// Shared envelope — present on all message-bearing entries
97//
98// parentUuid serialises WITHOUT skip_serializing_if so that explicit JSON
99// nulls (first message in a session) round-trip correctly as null rather
100// than being dropped.
101// ---------------------------------------------------------------------------
102
103#[derive(Debug, Clone, Serialize, Deserialize)]
104#[serde(rename_all = "camelCase")]
105pub struct Envelope {
106    pub uuid: String,
107
108    /// null = first message in session; UUID = linked to previous entry.
109    pub parent_uuid: Option<String>,
110
111    /// Preserves logical chain across compact boundaries (parentUuid is
112    /// nulled at those points).
113    #[serde(skip_serializing_if = "Option::is_none")]
114    pub logical_parent_uuid: Option<String>,
115
116    pub is_sidechain: bool,
117    pub session_id: String,
118    pub timestamp: String,
119
120    #[serde(skip_serializing_if = "Option::is_none")]
121    pub user_type: Option<String>,
122
123    #[serde(skip_serializing_if = "Option::is_none")]
124    pub entrypoint: Option<String>,
125
126    #[serde(skip_serializing_if = "Option::is_none")]
127    pub cwd: Option<String>,
128
129    #[serde(skip_serializing_if = "Option::is_none")]
130    pub version: Option<String>,
131
132    #[serde(skip_serializing_if = "Option::is_none")]
133    pub git_branch: Option<String>,
134
135    /// Human-readable session slug, e.g. "drifting-tinkering-parnas".
136    #[serde(skip_serializing_if = "Option::is_none")]
137    pub slug: Option<String>,
138
139    /// 7-char hex id for sidechain / subagent sessions.
140    #[serde(skip_serializing_if = "Option::is_none")]
141    pub agent_id: Option<String>,
142
143    #[serde(skip_serializing_if = "Option::is_none")]
144    pub team_name: Option<String>,
145
146    #[serde(skip_serializing_if = "Option::is_none")]
147    pub agent_name: Option<String>,
148
149    #[serde(skip_serializing_if = "Option::is_none")]
150    pub agent_color: Option<String>,
151
152    /// Correlates with OTel prompt.id for user-prompt messages.
153    #[serde(skip_serializing_if = "Option::is_none")]
154    pub prompt_id: Option<String>,
155
156    /// True when this entry should be hidden in the UI (meta / invisible).
157    #[serde(rename = "isMeta", skip_serializing_if = "Option::is_none")]
158    pub is_meta: Option<bool>,
159
160    /// Set when this session was forked from another session.
161    #[serde(rename = "forkedFrom", skip_serializing_if = "Option::is_none")]
162    pub forked_from: Option<ForkedFrom>,
163}
164
165#[derive(Debug, Clone, Serialize, Deserialize)]
166#[serde(rename_all = "camelCase")]
167pub struct ForkedFrom {
168    pub message_uuid: String,
169    pub session_id: String,
170}
171
172// ---------------------------------------------------------------------------
173// User entry
174// ---------------------------------------------------------------------------
175
176#[derive(Debug, Clone, Serialize, Deserialize)]
177pub struct UserEntry {
178    #[serde(flatten)]
179    pub envelope: Envelope,
180    pub message: UserMessage,
181
182    /// Structured result of the tool call this message delivers (populated
183    /// by Claude Code, not the API).
184    #[serde(rename = "toolUseResult", skip_serializing_if = "Option::is_none")]
185    pub tool_use_result: Option<Value>,
186
187    /// UUID of the assistant message that requested this tool result.
188    #[serde(
189        rename = "sourceToolAssistantUUID",
190        skip_serializing_if = "Option::is_none"
191    )]
192    pub source_tool_assistant_uuid: Option<String>,
193
194    /// ID of the tool use block that triggered this user message.
195    #[serde(rename = "sourceToolUseID", skip_serializing_if = "Option::is_none")]
196    pub source_tool_use_id: Option<String>,
197
198    #[serde(rename = "permissionMode", skip_serializing_if = "Option::is_none")]
199    pub permission_mode: Option<String>,
200
201    #[serde(skip_serializing_if = "Option::is_none")]
202    pub origin: Option<Value>,
203
204    #[serde(rename = "isCompactSummary", skip_serializing_if = "Option::is_none")]
205    pub is_compact_summary: Option<bool>,
206
207    #[serde(
208        rename = "isVisibleInTranscriptOnly",
209        skip_serializing_if = "Option::is_none"
210    )]
211    pub is_visible_in_transcript_only: Option<bool>,
212
213    #[serde(rename = "imagePasteIds", skip_serializing_if = "Option::is_none")]
214    pub image_paste_ids: Option<Vec<u64>>,
215
216    #[serde(rename = "planContent", skip_serializing_if = "Option::is_none")]
217    pub plan_content: Option<String>,
218}
219
220#[derive(Debug, Clone, Serialize, Deserialize)]
221pub struct UserMessage {
222    pub role: UserRole,
223    pub content: UserContent,
224}
225
226#[derive(Debug, Clone, Serialize, Deserialize)]
227#[serde(rename_all = "lowercase")]
228pub enum UserRole {
229    User,
230}
231
232/// User content is either a plain string or an array of typed blocks.
233#[derive(Debug, Clone, Serialize, Deserialize)]
234#[serde(untagged)]
235pub enum UserContent {
236    Text(String),
237    Blocks(Vec<UserContentBlock>),
238}
239
240#[derive(Debug, Clone, Serialize, Deserialize)]
241#[serde(tag = "type", rename_all = "snake_case")]
242pub enum UserContentBlock {
243    Text {
244        text: String,
245    },
246
247    ToolResult {
248        tool_use_id: String,
249        /// String for plain text, or array of content blocks for rich results.
250        /// Using Value here because serde cannot nest untagged enums inside
251        /// the fields of an internally-tagged enum variant.
252        content: Value,
253        #[serde(skip_serializing_if = "Option::is_none")]
254        is_error: Option<bool>,
255    },
256
257    Image {
258        source: ImageSource,
259    },
260
261    Document {
262        source: DocumentSource,
263        #[serde(skip_serializing_if = "Option::is_none")]
264        title: Option<String>,
265    },
266}
267
268#[derive(Debug, Clone, Serialize, Deserialize)]
269#[serde(tag = "type", rename_all = "snake_case")]
270pub enum ImageSource {
271    Base64 { media_type: String, data: String },
272    Url { url: String },
273}
274
275#[derive(Debug, Clone, Serialize, Deserialize)]
276#[serde(tag = "type", rename_all = "snake_case")]
277pub enum DocumentSource {
278    Base64 { media_type: String, data: String },
279    Text { data: String },
280    Url { url: String },
281}
282
283// ---------------------------------------------------------------------------
284// Assistant entry
285// ---------------------------------------------------------------------------
286
287#[derive(Debug, Clone, Serialize, Deserialize)]
288#[serde(rename_all = "camelCase")]
289pub struct AssistantEntry {
290    #[serde(flatten)]
291    pub envelope: Envelope,
292    pub message: AssistantMessage,
293
294    #[serde(skip_serializing_if = "Option::is_none")]
295    pub request_id: Option<String>,
296
297    #[serde(rename = "isApiErrorMessage", skip_serializing_if = "Option::is_none")]
298    pub is_api_error_message: Option<bool>,
299
300    #[serde(skip_serializing_if = "Option::is_none")]
301    pub error: Option<String>,
302}
303
304#[derive(Debug, Clone, Serialize, Deserialize)]
305pub struct AssistantMessage {
306    pub id: String,
307    /// Always "message".
308    #[serde(rename = "type")]
309    pub msg_type: String,
310    pub role: AssistantRole,
311    pub model: String,
312
313    /// null when no container; Some(None) = present as JSON null.
314    #[serde(
315        default,
316        skip_serializing_if = "Option::is_none",
317        with = "opt_nullable"
318    )]
319    pub container: Option<Option<Value>>,
320
321    pub content: Vec<AssistantContentBlock>,
322
323    /// The API always includes this field; null means the stream is still
324    /// ongoing or the field was not set.
325    pub stop_reason: Option<String>,
326
327    /// null when stop_reason != "stop_sequence"
328    pub stop_sequence: Option<String>,
329
330    /// null in most responses; some API versions emit structured details.
331    /// outer None = field absent, Some(None) = field present as JSON null.
332    #[serde(
333        default,
334        skip_serializing_if = "Option::is_none",
335        with = "opt_nullable"
336    )]
337    pub stop_details: Option<Option<Value>>,
338
339    pub usage: AssistantUsage,
340
341    /// null in most responses; Some(None) = present as JSON null.
342    #[serde(
343        default,
344        skip_serializing_if = "Option::is_none",
345        with = "opt_nullable"
346    )]
347    pub context_management: Option<Option<Value>>,
348}
349
350#[derive(Debug, Clone, Serialize, Deserialize)]
351#[serde(rename_all = "lowercase")]
352pub enum AssistantRole {
353    Assistant,
354}
355
356#[derive(Debug, Clone, Serialize, Deserialize)]
357#[serde(tag = "type", rename_all = "snake_case")]
358pub enum AssistantContentBlock {
359    Text {
360        text: String,
361    },
362
363    /// Extended thinking block. `thinking` is always an empty string in
364    /// persisted transcripts (Claude Code redacts it for storage); the
365    /// cryptographic `signature` is retained.
366    Thinking {
367        thinking: String,
368        signature: String,
369    },
370
371    RedactedThinking {
372        data: String,
373    },
374
375    ToolUse {
376        id: String,
377        name: String,
378        input: Value,
379        /// Present in some versions to identify call origin.
380        #[serde(skip_serializing_if = "Option::is_none")]
381        caller: Option<ToolUseCaller>,
382    },
383}
384
385#[derive(Debug, Clone, Serialize, Deserialize)]
386pub struct ToolUseCaller {
387    #[serde(rename = "type")]
388    pub caller_type: String,
389}
390
391// The Anthropic API returns usage fields in snake_case — no rename_all here.
392#[derive(Debug, Clone, Serialize, Deserialize)]
393pub struct AssistantUsage {
394    pub input_tokens: u64,
395    pub output_tokens: u64,
396
397    #[serde(skip_serializing_if = "Option::is_none")]
398    pub cache_creation_input_tokens: Option<u64>,
399
400    #[serde(skip_serializing_if = "Option::is_none")]
401    pub cache_read_input_tokens: Option<u64>,
402
403    #[serde(skip_serializing_if = "Option::is_none")]
404    pub server_tool_use: Option<ServerToolUse>,
405
406    /// null = explicitly set to null by API; absent = field not present.
407    #[serde(
408        default,
409        skip_serializing_if = "Option::is_none",
410        with = "opt_nullable"
411    )]
412    pub service_tier: Option<Option<Value>>,
413
414    #[serde(skip_serializing_if = "Option::is_none")]
415    pub cache_creation: Option<CacheCreation>,
416
417    /// null = explicitly set to null by API; absent = field not present.
418    #[serde(
419        default,
420        skip_serializing_if = "Option::is_none",
421        with = "opt_nullable"
422    )]
423    pub inference_geo: Option<Option<Value>>,
424
425    /// null = explicitly set to null by API; absent = field not present.
426    #[serde(
427        default,
428        skip_serializing_if = "Option::is_none",
429        with = "opt_nullable"
430    )]
431    pub iterations: Option<Option<Value>>,
432
433    /// null = explicitly set to null by API; absent = field not present.
434    #[serde(
435        default,
436        skip_serializing_if = "Option::is_none",
437        with = "opt_nullable"
438    )]
439    pub speed: Option<Option<Value>>,
440}
441
442#[derive(Debug, Clone, Serialize, Deserialize)]
443pub struct ServerToolUse {
444    pub web_search_requests: u64,
445    pub web_fetch_requests: u64,
446}
447
448#[derive(Debug, Clone, Serialize, Deserialize)]
449pub struct CacheCreation {
450    #[serde(skip_serializing_if = "Option::is_none")]
451    pub ephemeral_1h_input_tokens: Option<u64>,
452    #[serde(skip_serializing_if = "Option::is_none")]
453    pub ephemeral_5m_input_tokens: Option<u64>,
454}
455
456#[derive(Debug, Clone, Serialize, Deserialize)]
457pub struct UsageIteration {
458    pub input_tokens: u64,
459    pub output_tokens: u64,
460
461    #[serde(skip_serializing_if = "Option::is_none")]
462    pub cache_read_input_tokens: Option<u64>,
463
464    #[serde(skip_serializing_if = "Option::is_none")]
465    pub cache_creation_input_tokens: Option<u64>,
466
467    #[serde(skip_serializing_if = "Option::is_none")]
468    pub cache_creation: Option<CacheCreation>,
469
470    /// Iteration type; typically "message".
471    #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
472    pub iter_type: Option<String>,
473}
474
475// ---------------------------------------------------------------------------
476// System entry
477//
478// All subtype-specific fields are optional so a single flat struct covers
479// every subtype while preserving exact field order semantics.  Type safety
480// on the discriminant is still enforced via SystemSubtype.
481// ---------------------------------------------------------------------------
482
483#[derive(Debug, Clone, Serialize, Deserialize)]
484#[serde(rename_all = "camelCase")]
485pub struct SystemEntry {
486    #[serde(flatten)]
487    pub envelope: Envelope,
488
489    pub subtype: SystemSubtype,
490
491    /// Human-readable message text (most subtypes).
492    #[serde(skip_serializing_if = "Option::is_none")]
493    pub content: Option<String>,
494
495    /// Severity level: "info" | "warning" | "error" | "suggestion".
496    #[serde(skip_serializing_if = "Option::is_none")]
497    pub level: Option<String>,
498
499    /// True when the entry should be hidden from the main conversation view.
500    #[serde(rename = "isMeta", skip_serializing_if = "Option::is_none")]
501    pub is_meta: Option<bool>,
502
503    // ── api_error ────────────────────────────────────────────────────────
504    #[serde(skip_serializing_if = "Option::is_none")]
505    pub cause: Option<Value>,
506
507    #[serde(skip_serializing_if = "Option::is_none")]
508    pub error: Option<Value>,
509
510    #[serde(rename = "retryInMs", skip_serializing_if = "Option::is_none")]
511    pub retry_in_ms: Option<f64>,
512
513    #[serde(rename = "retryAttempt", skip_serializing_if = "Option::is_none")]
514    pub retry_attempt: Option<u32>,
515
516    #[serde(rename = "maxRetries", skip_serializing_if = "Option::is_none")]
517    pub max_retries: Option<u32>,
518
519    // ── stop_hook_summary ────────────────────────────────────────────────
520    #[serde(rename = "hookCount", skip_serializing_if = "Option::is_none")]
521    pub hook_count: Option<u32>,
522
523    #[serde(rename = "hookInfos", skip_serializing_if = "Option::is_none")]
524    pub hook_infos: Option<Vec<HookInfo>>,
525
526    #[serde(rename = "hookErrors", skip_serializing_if = "Option::is_none")]
527    pub hook_errors: Option<Vec<Value>>,
528
529    #[serde(
530        rename = "preventedContinuation",
531        skip_serializing_if = "Option::is_none"
532    )]
533    pub prevented_continuation: Option<bool>,
534
535    #[serde(rename = "stopReason", skip_serializing_if = "Option::is_none")]
536    pub stop_reason: Option<String>,
537
538    #[serde(rename = "hasOutput", skip_serializing_if = "Option::is_none")]
539    pub has_output: Option<bool>,
540
541    #[serde(rename = "toolUseID", skip_serializing_if = "Option::is_none")]
542    pub tool_use_id: Option<String>,
543
544    // ── turn_duration ────────────────────────────────────────────────────
545    #[serde(rename = "durationMs", skip_serializing_if = "Option::is_none")]
546    pub duration_ms: Option<f64>,
547
548    #[serde(rename = "messageCount", skip_serializing_if = "Option::is_none")]
549    pub message_count: Option<u32>,
550
551    // ── bridge_status ────────────────────────────────────────────────────
552    #[serde(skip_serializing_if = "Option::is_none")]
553    pub url: Option<String>,
554
555    #[serde(rename = "upgradeNudge", skip_serializing_if = "Option::is_none")]
556    pub upgrade_nudge: Option<String>,
557
558    // ── compact_boundary ────────────────────────────────────────────────
559    #[serde(rename = "compactMetadata", skip_serializing_if = "Option::is_none")]
560    pub compact_metadata: Option<CompactMetadata>,
561}
562
563#[derive(Debug, Clone, Serialize, Deserialize)]
564#[serde(rename_all = "snake_case")]
565pub enum SystemSubtype {
566    ApiError,
567    AwaySummary,
568    BridgeStatus,
569    CompactBoundary,
570    Informational,
571    LocalCommand,
572    ScheduledTaskFire,
573    StopHookSummary,
574    TurnDuration,
575    MicrocompactBoundary,
576    PermissionRetry,
577    AgentsKilled,
578    #[serde(other)]
579    Unknown,
580}
581
582#[derive(Debug, Clone, Serialize, Deserialize)]
583#[serde(rename_all = "camelCase")]
584pub struct HookInfo {
585    pub command: String,
586    pub duration_ms: u64,
587}
588
589#[derive(Debug, Clone, Serialize, Deserialize)]
590#[serde(rename_all = "camelCase")]
591pub struct PreservedSegment {
592    pub head_uuid: String,
593    pub anchor_uuid: String,
594    pub tail_uuid: String,
595}
596
597#[derive(Debug, Clone, Serialize, Deserialize)]
598#[serde(rename_all = "camelCase")]
599pub struct CompactMetadata {
600    pub trigger: String,
601    #[serde(skip_serializing_if = "Option::is_none")]
602    pub pre_tokens: Option<u64>,
603    #[serde(skip_serializing_if = "Option::is_none")]
604    pub post_tokens: Option<u64>,
605    #[serde(skip_serializing_if = "Option::is_none")]
606    pub duration_ms: Option<u64>,
607    #[serde(skip_serializing_if = "Option::is_none")]
608    pub preserved_segment: Option<PreservedSegment>,
609    #[serde(
610        rename = "preCompactDiscoveredTools",
611        skip_serializing_if = "Option::is_none"
612    )]
613    pub pre_compact_discovered_tools: Option<Vec<String>>,
614}
615
616// ---------------------------------------------------------------------------
617// Attachment entry
618// ---------------------------------------------------------------------------
619
620#[derive(Debug, Clone, Serialize, Deserialize)]
621pub struct AttachmentEntry {
622    #[serde(flatten)]
623    pub envelope: Envelope,
624    pub attachment: AttachmentData,
625}
626
627#[derive(Debug, Clone, Serialize, Deserialize)]
628#[serde(tag = "type", rename_all = "snake_case")]
629pub enum AttachmentData {
630    // ── Hook results ─────────────────────────────────────────────────────
631    HookSuccess(HookResultAttachment),
632    HookNonBlockingError(HookResultAttachment),
633    HookBlockingError(HookResultAttachment),
634    HookCancelled(HookResultAttachment),
635
636    HookAdditionalContext {
637        content: Vec<String>,
638        #[serde(rename = "hookName", skip_serializing_if = "Option::is_none")]
639        hook_name: Option<String>,
640        #[serde(rename = "toolUseID", skip_serializing_if = "Option::is_none")]
641        tool_use_id: Option<String>,
642        #[serde(rename = "hookEvent", skip_serializing_if = "Option::is_none")]
643        hook_event: Option<String>,
644    },
645
646    HookPermissionDecision {
647        decision: String,
648        #[serde(rename = "hookName", skip_serializing_if = "Option::is_none")]
649        hook_name: Option<String>,
650        #[serde(rename = "toolUseID", skip_serializing_if = "Option::is_none")]
651        tool_use_id: Option<String>,
652        #[serde(rename = "hookEvent", skip_serializing_if = "Option::is_none")]
653        hook_event: Option<String>,
654    },
655
656    // ── File / filesystem ────────────────────────────────────────────────
657    File {
658        filename: String,
659        content: FileAttachmentContent,
660        #[serde(rename = "displayPath", skip_serializing_if = "Option::is_none")]
661        display_path: Option<String>,
662    },
663
664    EditedTextFile {
665        filename: String,
666        /// Line-numbered file content snippet.
667        snippet: String,
668    },
669
670    Directory {
671        path: String,
672        content: String,
673        #[serde(rename = "displayPath")]
674        display_path: String,
675    },
676
677    CompactFileReference {
678        filename: String,
679        #[serde(rename = "displayPath")]
680        display_path: String,
681    },
682
683    // ── Permissions ──────────────────────────────────────────────────────
684    CommandPermissions {
685        #[serde(rename = "allowedTools")]
686        allowed_tools: Vec<String>,
687    },
688
689    // ── Plan mode ────────────────────────────────────────────────────────
690    PlanMode {
691        #[serde(rename = "reminderType")]
692        reminder_type: String,
693        #[serde(rename = "isSubAgent")]
694        is_sub_agent: bool,
695        #[serde(rename = "planFilePath", skip_serializing_if = "Option::is_none")]
696        plan_file_path: Option<String>,
697        #[serde(rename = "planExists")]
698        plan_exists: bool,
699    },
700
701    PlanModeExit {
702        #[serde(rename = "planFilePath", skip_serializing_if = "Option::is_none")]
703        plan_file_path: Option<String>,
704        #[serde(rename = "planExists")]
705        plan_exists: bool,
706    },
707
708    // ── Skills ───────────────────────────────────────────────────────────
709    SkillListing {
710        content: String,
711        /// True on the very first skill listing injection for a session.
712        #[serde(rename = "isInitial", skip_serializing_if = "Option::is_none")]
713        is_initial: Option<bool>,
714        /// Total number of skills listed.
715        #[serde(rename = "skillCount", skip_serializing_if = "Option::is_none")]
716        skill_count: Option<u32>,
717    },
718
719    DynamicSkill {
720        #[serde(rename = "skillDir")]
721        skill_dir: String,
722        #[serde(rename = "skillNames")]
723        skill_names: Vec<String>,
724        #[serde(rename = "displayPath")]
725        display_path: String,
726    },
727
728    InvokedSkills {
729        skills: Vec<InvokedSkill>,
730    },
731
732    // ── Tasks ────────────────────────────────────────────────────────────
733    TaskReminder {
734        content: Vec<Value>,
735        #[serde(rename = "itemCount")]
736        item_count: u32,
737    },
738
739    // ── Diagnostics / IDE ────────────────────────────────────────────────
740    Diagnostics {
741        files: Vec<DiagnosticsFile>,
742        #[serde(rename = "isNew")]
743        is_new: bool,
744    },
745
746    // ── Dates / context ──────────────────────────────────────────────────
747    DateChange {
748        #[serde(rename = "newDate")]
749        new_date: String,
750    },
751
752    // ── Tool / MCP updates ───────────────────────────────────────────────
753    DeferredToolsDelta {
754        #[serde(rename = "addedNames")]
755        added_names: Vec<String>,
756        /// Legacy/alias field that mirrors addedNames; both are present in
757        /// some versions.
758        #[serde(rename = "addedLines", skip_serializing_if = "Option::is_none")]
759        added_lines: Option<Vec<String>>,
760        #[serde(rename = "removedNames", skip_serializing_if = "Option::is_none")]
761        removed_names: Option<Vec<String>>,
762    },
763
764    McpInstructionsDelta {
765        #[serde(rename = "addedNames")]
766        added_names: Vec<String>,
767        #[serde(rename = "addedBlocks")]
768        added_blocks: Vec<String>,
769        #[serde(rename = "removedNames", skip_serializing_if = "Option::is_none")]
770        removed_names: Option<Vec<String>>,
771    },
772
773    // ── Thinking effort ──────────────────────────────────────────────────
774    UltrathinkEffort {
775        level: String,
776    },
777
778    // ── Queued commands ──────────────────────────────────────────────────
779    QueuedCommand {
780        prompt: String,
781        #[serde(rename = "commandMode", skip_serializing_if = "Option::is_none")]
782        command_mode: Option<String>,
783    },
784}
785
786#[derive(Debug, Clone, Serialize, Deserialize)]
787#[serde(rename_all = "camelCase")]
788pub struct HookResultAttachment {
789    #[serde(rename = "hookName", skip_serializing_if = "Option::is_none")]
790    pub hook_name: Option<String>,
791    #[serde(rename = "toolUseID", skip_serializing_if = "Option::is_none")]
792    pub tool_use_id: Option<String>,
793    #[serde(rename = "hookEvent", skip_serializing_if = "Option::is_none")]
794    pub hook_event: Option<String>,
795    #[serde(skip_serializing_if = "Option::is_none")]
796    pub content: Option<String>,
797    #[serde(skip_serializing_if = "Option::is_none")]
798    pub stdout: Option<String>,
799    #[serde(skip_serializing_if = "Option::is_none")]
800    pub stderr: Option<String>,
801    #[serde(skip_serializing_if = "Option::is_none")]
802    pub exit_code: Option<i32>,
803    #[serde(skip_serializing_if = "Option::is_none")]
804    pub command: Option<String>,
805    #[serde(skip_serializing_if = "Option::is_none")]
806    pub duration_ms: Option<u64>,
807    #[serde(rename = "blockingError", skip_serializing_if = "Option::is_none")]
808    pub blocking_error: Option<Value>,
809}
810
811/// Wrapper for a file content attachment.
812#[derive(Debug, Clone, Serialize, Deserialize)]
813#[serde(rename_all = "camelCase")]
814pub struct FileAttachmentContent {
815    #[serde(rename = "type")]
816    pub content_type: String,
817    pub file: FileData,
818}
819
820#[derive(Debug, Clone, Serialize, Deserialize)]
821#[serde(rename_all = "camelCase")]
822pub struct FileData {
823    pub file_path: String,
824    #[serde(skip_serializing_if = "Option::is_none")]
825    pub content: Option<String>,
826    #[serde(rename = "numLines", skip_serializing_if = "Option::is_none")]
827    pub num_lines: Option<u64>,
828    #[serde(rename = "startLine", skip_serializing_if = "Option::is_none")]
829    pub start_line: Option<u64>,
830    #[serde(rename = "totalLines", skip_serializing_if = "Option::is_none")]
831    pub total_lines: Option<u64>,
832}
833
834#[derive(Debug, Clone, Serialize, Deserialize)]
835pub struct InvokedSkill {
836    pub name: String,
837    pub path: String,
838    pub content: String,
839}
840
841#[derive(Debug, Clone, Serialize, Deserialize)]
842pub struct DiagnosticsFile {
843    pub uri: String,
844    pub diagnostics: Vec<Diagnostic>,
845}
846
847#[derive(Debug, Clone, Serialize, Deserialize)]
848pub struct Diagnostic {
849    pub message: String,
850    pub severity: String,
851    pub range: DiagnosticRange,
852    #[serde(skip_serializing_if = "Option::is_none")]
853    pub source: Option<String>,
854    #[serde(skip_serializing_if = "Option::is_none")]
855    pub code: Option<Value>,
856}
857
858#[derive(Debug, Clone, Serialize, Deserialize)]
859pub struct DiagnosticRange {
860    pub start: DiagnosticPosition,
861    pub end: DiagnosticPosition,
862}
863
864#[derive(Debug, Clone, Serialize, Deserialize)]
865pub struct DiagnosticPosition {
866    pub line: u32,
867    pub character: u32,
868}
869
870// ---------------------------------------------------------------------------
871// Progress entry
872// ---------------------------------------------------------------------------
873
874#[derive(Debug, Clone, Serialize, Deserialize)]
875#[serde(rename_all = "camelCase")]
876pub struct ProgressEntry {
877    #[serde(flatten)]
878    pub envelope: Envelope,
879
880    pub data: ProgressData,
881
882    #[serde(rename = "parentToolUseID", skip_serializing_if = "Option::is_none")]
883    pub parent_tool_use_id: Option<String>,
884
885    #[serde(rename = "toolUseID", skip_serializing_if = "Option::is_none")]
886    pub tool_use_id: Option<String>,
887}
888
889#[derive(Debug, Clone, Serialize, Deserialize)]
890#[serde(rename_all = "camelCase")]
891pub struct ProgressData {
892    #[serde(rename = "type")]
893    pub data_type: String,
894    #[serde(rename = "hookEvent", skip_serializing_if = "Option::is_none")]
895    pub hook_event: Option<String>,
896    #[serde(rename = "hookName", skip_serializing_if = "Option::is_none")]
897    pub hook_name: Option<String>,
898    #[serde(skip_serializing_if = "Option::is_none")]
899    pub command: Option<String>,
900    // agent_progress fields
901    #[serde(rename = "agentId", skip_serializing_if = "Option::is_none")]
902    pub agent_id: Option<String>,
903    #[serde(skip_serializing_if = "Option::is_none")]
904    pub prompt: Option<String>,
905    #[serde(skip_serializing_if = "Option::is_none")]
906    pub message: Option<Value>,
907    // query_update / search progress fields
908    #[serde(skip_serializing_if = "Option::is_none")]
909    pub query: Option<String>,
910    #[serde(rename = "resultCount", skip_serializing_if = "Option::is_none")]
911    pub result_count: Option<u32>,
912    // bash/command progress fields
913    #[serde(rename = "elapsedTimeSeconds", skip_serializing_if = "Option::is_none")]
914    pub elapsed_time_seconds: Option<f64>,
915    #[serde(rename = "fullOutput", skip_serializing_if = "Option::is_none")]
916    pub full_output: Option<String>,
917    #[serde(rename = "output", skip_serializing_if = "Option::is_none")]
918    pub output: Option<String>,
919    #[serde(rename = "timeoutMs", skip_serializing_if = "Option::is_none")]
920    pub timeout_ms: Option<u64>,
921    #[serde(rename = "totalLines", skip_serializing_if = "Option::is_none")]
922    pub total_lines: Option<u64>,
923    #[serde(rename = "totalBytes", skip_serializing_if = "Option::is_none")]
924    pub total_bytes: Option<u64>,
925    #[serde(rename = "taskId", skip_serializing_if = "Option::is_none")]
926    pub task_id: Option<String>,
927    // mcp tool progress fields
928    #[serde(rename = "serverName", skip_serializing_if = "Option::is_none")]
929    pub server_name: Option<String>,
930    #[serde(rename = "status", skip_serializing_if = "Option::is_none")]
931    pub status: Option<String>,
932    #[serde(rename = "toolName", skip_serializing_if = "Option::is_none")]
933    pub tool_name: Option<String>,
934    #[serde(rename = "elapsedTimeMs", skip_serializing_if = "Option::is_none")]
935    pub elapsed_time_ms: Option<f64>,
936    // agent task progress fields
937    #[serde(rename = "taskDescription", skip_serializing_if = "Option::is_none")]
938    pub task_description: Option<String>,
939    #[serde(rename = "taskType", skip_serializing_if = "Option::is_none")]
940    pub task_type: Option<String>,
941}
942
943// ---------------------------------------------------------------------------
944// Metadata-only entries
945// ---------------------------------------------------------------------------
946
947#[derive(Debug, Clone, Serialize, Deserialize)]
948#[serde(rename_all = "camelCase")]
949pub struct PermissionModeEntry {
950    pub permission_mode: String,
951    pub session_id: String,
952}
953
954#[derive(Debug, Clone, Serialize, Deserialize)]
955#[serde(rename_all = "camelCase")]
956pub struct LastPromptEntry {
957    pub last_prompt: String,
958    pub session_id: String,
959}
960
961#[derive(Debug, Clone, Serialize, Deserialize)]
962#[serde(rename_all = "camelCase")]
963pub struct AiTitleEntry {
964    pub ai_title: String,
965    pub session_id: String,
966}
967
968#[derive(Debug, Clone, Serialize, Deserialize)]
969#[serde(rename_all = "camelCase")]
970pub struct CustomTitleEntry {
971    pub custom_title: String,
972    pub session_id: String,
973}
974
975#[derive(Debug, Clone, Serialize, Deserialize)]
976#[serde(rename_all = "camelCase")]
977pub struct AgentNameEntry {
978    pub agent_name: String,
979    pub session_id: String,
980}
981
982#[derive(Debug, Clone, Serialize, Deserialize)]
983#[serde(rename_all = "camelCase")]
984pub struct AgentColorEntry {
985    pub agent_color: String,
986    pub session_id: String,
987}
988
989#[derive(Debug, Clone, Serialize, Deserialize)]
990#[serde(rename_all = "camelCase")]
991pub struct AgentSettingEntry {
992    pub agent_setting: String,
993    pub session_id: String,
994}
995
996#[derive(Debug, Clone, Serialize, Deserialize)]
997#[serde(rename_all = "camelCase")]
998pub struct TagEntry {
999    pub tag: String,
1000    pub session_id: String,
1001}
1002
1003#[derive(Debug, Clone, Serialize, Deserialize)]
1004#[serde(rename_all = "camelCase")]
1005pub struct SummaryEntry {
1006    pub leaf_uuid: String,
1007    pub summary: String,
1008    pub session_id: String,
1009}
1010
1011#[derive(Debug, Clone, Serialize, Deserialize)]
1012#[serde(rename_all = "camelCase")]
1013pub struct TaskSummaryEntry {
1014    pub summary: String,
1015    pub session_id: String,
1016    pub timestamp: String,
1017}
1018
1019#[derive(Debug, Clone, Serialize, Deserialize)]
1020#[serde(rename_all = "camelCase")]
1021pub struct PrLinkEntry {
1022    pub session_id: String,
1023    pub pr_number: u32,
1024    pub pr_url: String,
1025    pub pr_repository: String,
1026    pub timestamp: String,
1027}
1028
1029#[derive(Debug, Clone, Serialize, Deserialize)]
1030#[serde(rename_all = "camelCase")]
1031pub struct ModeEntry {
1032    pub mode: SessionMode,
1033    pub session_id: String,
1034}
1035
1036#[derive(Debug, Clone, Serialize, Deserialize)]
1037#[serde(rename_all = "lowercase")]
1038pub enum SessionMode {
1039    Coordinator,
1040    Normal,
1041}
1042
1043// worktreeSession is nullable (null = exited, object = active)
1044#[derive(Debug, Clone, Serialize, Deserialize)]
1045#[serde(rename_all = "camelCase")]
1046pub struct WorktreeStateEntry {
1047    pub session_id: String,
1048    /// null when the worktree session was exited; Some when active.
1049    pub worktree_session: Option<PersistedWorktreeSession>,
1050}
1051
1052#[derive(Debug, Clone, Serialize, Deserialize)]
1053#[serde(rename_all = "camelCase")]
1054pub struct PersistedWorktreeSession {
1055    pub original_cwd: String,
1056    pub worktree_path: String,
1057    pub worktree_name: String,
1058    pub session_id: String,
1059
1060    #[serde(skip_serializing_if = "Option::is_none")]
1061    pub worktree_branch: Option<String>,
1062
1063    #[serde(skip_serializing_if = "Option::is_none")]
1064    pub original_branch: Option<String>,
1065
1066    #[serde(skip_serializing_if = "Option::is_none")]
1067    pub original_head_commit: Option<String>,
1068
1069    #[serde(rename = "tmuxSessionName", skip_serializing_if = "Option::is_none")]
1070    pub tmux_session_name: Option<String>,
1071
1072    #[serde(skip_serializing_if = "Option::is_none")]
1073    pub hook_based: Option<bool>,
1074}
1075
1076#[derive(Debug, Clone, Serialize, Deserialize)]
1077#[serde(rename_all = "camelCase")]
1078pub struct ContentReplacementEntry {
1079    pub session_id: String,
1080    pub replacements: Vec<Value>,
1081    #[serde(skip_serializing_if = "Option::is_none")]
1082    pub agent_id: Option<String>,
1083}
1084
1085#[derive(Debug, Clone, Serialize, Deserialize)]
1086#[serde(rename_all = "camelCase")]
1087pub struct FileHistorySnapshotEntry {
1088    pub message_id: String,
1089    pub snapshot: FileHistorySnapshot,
1090    pub is_snapshot_update: bool,
1091}
1092
1093#[derive(Debug, Clone, Serialize, Deserialize)]
1094#[serde(rename_all = "camelCase")]
1095pub struct FileHistorySnapshot {
1096    pub message_id: String,
1097    pub tracked_file_backups: Value,
1098    pub timestamp: String,
1099}
1100
1101#[derive(Debug, Clone, Serialize, Deserialize)]
1102#[serde(rename_all = "camelCase")]
1103pub struct AttributionSnapshotEntry {
1104    pub message_id: String,
1105    pub surface: String,
1106    pub file_states: Value,
1107
1108    #[serde(skip_serializing_if = "Option::is_none")]
1109    pub prompt_count: Option<u32>,
1110
1111    #[serde(skip_serializing_if = "Option::is_none")]
1112    pub prompt_count_at_last_commit: Option<u32>,
1113
1114    #[serde(skip_serializing_if = "Option::is_none")]
1115    pub permission_prompt_count: Option<u32>,
1116
1117    #[serde(skip_serializing_if = "Option::is_none")]
1118    pub permission_prompt_count_at_last_commit: Option<u32>,
1119
1120    #[serde(skip_serializing_if = "Option::is_none")]
1121    pub escape_count: Option<u32>,
1122
1123    #[serde(skip_serializing_if = "Option::is_none")]
1124    pub escape_count_at_last_commit: Option<u32>,
1125}
1126
1127#[derive(Debug, Clone, Serialize, Deserialize)]
1128#[serde(rename_all = "camelCase")]
1129pub struct QueueOperationEntry {
1130    pub operation: String,
1131    pub timestamp: String,
1132    pub session_id: String,
1133    #[serde(skip_serializing_if = "Option::is_none")]
1134    pub content: Option<String>,
1135}
1136
1137// ---------------------------------------------------------------------------
1138// Context-collapse entries (internal, obfuscated type names)
1139// ---------------------------------------------------------------------------
1140
1141#[derive(Debug, Clone, Serialize, Deserialize)]
1142#[serde(rename_all = "camelCase")]
1143pub struct ContextCollapseCommitEntry {
1144    pub session_id: String,
1145    pub collapse_id: String,
1146    pub summary_uuid: String,
1147    pub summary_content: String,
1148    pub summary: String,
1149    pub first_archived_uuid: String,
1150    pub last_archived_uuid: String,
1151}
1152
1153#[derive(Debug, Clone, Serialize, Deserialize)]
1154#[serde(rename_all = "camelCase")]
1155pub struct ContextCollapseSnapshotEntry {
1156    pub session_id: String,
1157    pub staged: Vec<StagedSpan>,
1158    pub armed: bool,
1159    pub last_spawn_tokens: u64,
1160}
1161
1162#[derive(Debug, Clone, Serialize, Deserialize)]
1163#[serde(rename_all = "camelCase")]
1164pub struct StagedSpan {
1165    pub start_uuid: String,
1166    pub end_uuid: String,
1167    pub summary: String,
1168    pub risk: f64,
1169    pub staged_at: u64,
1170}
1171
1172#[derive(Debug, Clone, Serialize, Deserialize)]
1173#[serde(rename_all = "camelCase")]
1174pub struct SpeculationAcceptEntry {
1175    pub timestamp: String,
1176    pub time_saved_ms: u64,
1177}
1178
1179// ---------------------------------------------------------------------------
1180// Serde helper: distinguish JSON null from absent field
1181//
1182// Used with:
1183//   #[serde(default, skip_serializing_if = "Option::is_none", with = "opt_nullable")]
1184//   pub field: Option<Option<T>>,
1185//
1186// Semantics:
1187//   None           → field absent  (skip_serializing_if prevents serialization)
1188//   Some(None)     → field present as JSON null
1189//   Some(Some(v))  → field present with value v
1190// ---------------------------------------------------------------------------
1191mod opt_nullable {
1192    use serde::{Deserialize, Deserializer, Serialize, Serializer};
1193    use serde_json::Value;
1194
1195    pub fn serialize<S>(val: &Option<Option<Value>>, ser: S) -> Result<S::Ok, S::Error>
1196    where
1197        S: Serializer,
1198    {
1199        match val {
1200            None => unreachable!("skip_serializing_if = \"Option::is_none\" should prevent this"),
1201            Some(inner) => inner.serialize(ser),
1202        }
1203    }
1204
1205    pub fn deserialize<'de, D>(de: D) -> Result<Option<Option<Value>>, D::Error>
1206    where
1207        D: Deserializer<'de>,
1208    {
1209        Ok(Some(Option::<Value>::deserialize(de)?))
1210    }
1211}