Skip to main content

supercode_harness/schema/
claude_code.rs

1//! Typed schema for Claude Code transcript JSONL
2//! (`~/.claude/projects/<encoded-cwd>/<id>.jsonl`).
3//!
4//! Each line is a record discriminated by `type`. `user` and `assistant` carry
5//! the conversation; the rest are state/metadata/UI events. Unmodeled record
6//! types land in [`ClaudeRecord::Unknown`]; unmodeled fields land in `extra`.
7
8use serde::Deserialize;
9
10use super::ExtraFields;
11
12/// One line of a Claude Code transcript.
13#[derive(Debug, Clone, Deserialize)]
14#[serde(tag = "type", rename_all = "kebab-case")]
15pub enum ClaudeRecord {
16    /// A user turn (plain text, or an array of content blocks incl. tool results).
17    User {
18        /// The message envelope.
19        message: Message,
20        /// Conversation-graph links and metadata.
21        #[serde(flatten)]
22        meta: RecordMeta,
23    },
24    /// An assistant turn (text, thinking, and/or tool calls).
25    Assistant {
26        /// The message envelope.
27        message: Message,
28        /// Conversation-graph links and metadata.
29        #[serde(flatten)]
30        meta: RecordMeta,
31    },
32    /// A system event (subtypes: `compact_boundary`, `turn_duration`,
33    /// `api_error`, `away_summary`, `stop_hook_summary`, …).
34    System {
35        /// The event subtype.
36        #[serde(default)]
37        subtype: Option<String>,
38        /// Anything else.
39        #[serde(flatten)]
40        extra: ExtraFields,
41    },
42    /// An attachment record (pasted/added files, images, command output).
43    Attachment {
44        /// Anything (shape varies widely).
45        #[serde(flatten)]
46        extra: ExtraFields,
47    },
48    /// A file-state snapshot enabling edit undo.
49    FileHistorySnapshot {
50        /// Anything.
51        #[serde(flatten)]
52        extra: ExtraFields,
53    },
54    /// The per-file half of the same edit-undo state: one record per file
55    /// touched by a turn, pointing back at the [`ClaudeRecord::FileHistorySnapshot`]
56    /// it belongs to (`snapshotMessageId`) and naming the tracked path and its
57    /// backup (`trackingPath`, `backup`). Claude Code began emitting these on
58    /// 2026-07-25; before that a snapshot carried the whole set. Like its
59    /// sibling it is a marker for `/rewind`, not conversation, so it is
60    /// modeled generically — the point is that it stops landing in the
61    /// anonymous [`ClaudeRecord::Unknown`] bucket.
62    FileHistoryDelta {
63        /// Anything.
64        #[serde(flatten)]
65        extra: ExtraFields,
66    },
67    /// Generated conversation title.
68    AiTitle {
69        /// Anything.
70        #[serde(flatten)]
71        extra: ExtraFields,
72    },
73    /// Permission-mode marker.
74    PermissionMode {
75        /// Anything.
76        #[serde(flatten)]
77        extra: ExtraFields,
78    },
79    /// Mode marker.
80    Mode {
81        /// Anything.
82        #[serde(flatten)]
83        extra: ExtraFields,
84    },
85    /// The last typed prompt (UI restore).
86    LastPrompt {
87        /// Anything.
88        #[serde(flatten)]
89        extra: ExtraFields,
90    },
91    /// Prompt-queue operation (UI).
92    QueueOperation {
93        /// Anything.
94        #[serde(flatten)]
95        extra: ExtraFields,
96    },
97    /// PR link metadata.
98    PrLink {
99        /// Anything.
100        #[serde(flatten)]
101        extra: ExtraFields,
102    },
103    /// Claude-hosted HTML frame link metadata (`path` + `frameUrl`). This is
104    /// session/UI state rather than a conversational turn, analogous to
105    /// [`ClaudeRecord::PrLink`], but remains explicitly typed so audits name
106    /// its intentional cross-format residue instead of reporting an unknown
107    /// record discriminant.
108    FrameLink {
109        /// Anything.
110        #[serde(flatten)]
111        extra: ExtraFields,
112    },
113    /// Subagent name marker.
114    AgentName {
115        /// Anything.
116        #[serde(flatten)]
117        extra: ExtraFields,
118    },
119    /// Subagent task start marker.
120    Started {
121        /// Anything.
122        #[serde(flatten)]
123        extra: ExtraFields,
124    },
125    /// Subagent task result marker.
126    Result {
127        /// Anything.
128        #[serde(flatten)]
129        extra: ExtraFields,
130    },
131    /// Git worktree state marker.
132    WorktreeState {
133        /// Anything.
134        #[serde(flatten)]
135        extra: ExtraFields,
136    },
137    /// PARITY-10 (provenance P010): a lineage/provenance marker linking this
138    /// session to the conversation context it was forked from (e.g. Claude
139    /// Code's `--fork-session` / rewind-and-branch flow). Extremely rare in
140    /// real corpora (observed 5 times in a ~12,300-file, ~1.3M-record
141    /// reference corpus) and its exact field shape is unconfirmed by any
142    /// sample this crate has seen — modeled generically (like
143    /// [`ClaudeRecord::WorktreeState`]/[`ClaudeRecord::AgentName`]) so every
144    /// field it carries, whatever they turn out to be, is captured in
145    /// `extra` rather than silently landing in the anonymous
146    /// [`ClaudeRecord::Unknown`] bucket.
147    ForkContextRef {
148        /// Anything.
149        #[serde(flatten)]
150        extra: ExtraFields,
151    },
152    /// Any record type we do not model yet.
153    #[serde(other)]
154    Unknown,
155}
156
157impl ClaudeRecord {
158    /// The static discriminant name, or `None` for [`ClaudeRecord::Unknown`].
159    pub fn tag(&self) -> Option<&'static str> {
160        Some(match self {
161            ClaudeRecord::User { .. } => "user",
162            ClaudeRecord::Assistant { .. } => "assistant",
163            ClaudeRecord::System { .. } => "system",
164            ClaudeRecord::Attachment { .. } => "attachment",
165            ClaudeRecord::FileHistorySnapshot { .. } => "file-history-snapshot",
166            ClaudeRecord::FileHistoryDelta { .. } => "file-history-delta",
167            ClaudeRecord::AiTitle { .. } => "ai-title",
168            ClaudeRecord::PermissionMode { .. } => "permission-mode",
169            ClaudeRecord::Mode { .. } => "mode",
170            ClaudeRecord::LastPrompt { .. } => "last-prompt",
171            ClaudeRecord::QueueOperation { .. } => "queue-operation",
172            ClaudeRecord::PrLink { .. } => "pr-link",
173            ClaudeRecord::FrameLink { .. } => "frame-link",
174            ClaudeRecord::AgentName { .. } => "agent-name",
175            ClaudeRecord::Started { .. } => "started",
176            ClaudeRecord::Result { .. } => "result",
177            ClaudeRecord::WorktreeState { .. } => "worktree-state",
178            ClaudeRecord::ForkContextRef { .. } => "fork-context-ref",
179            ClaudeRecord::Unknown => return None,
180        })
181    }
182}
183
184/// Conversation-graph links and per-record metadata shared by user/assistant.
185#[derive(Debug, Clone, Deserialize)]
186pub struct RecordMeta {
187    /// This record's id.
188    #[serde(default)]
189    pub uuid: Option<String>,
190    /// Parent record id (forms the conversation tree).
191    #[serde(default)]
192    pub parent_uuid: Option<String>,
193    /// Whether this record belongs to a subagent (Task) sidechain rather than
194    /// the main thread. **The loader does not yet separate sidechains.**
195    #[serde(default)]
196    pub is_sidechain: bool,
197    /// Session id.
198    #[serde(default)]
199    pub session_id: Option<String>,
200    /// Working directory.
201    #[serde(default)]
202    pub cwd: Option<String>,
203    /// Git branch at the time.
204    #[serde(default)]
205    pub git_branch: Option<String>,
206    /// Anything else.
207    #[serde(flatten)]
208    pub extra: ExtraFields,
209}
210
211/// The inner `message` object of a user/assistant record (Anthropic shape).
212#[derive(Debug, Clone, Deserialize)]
213pub struct Message {
214    /// `user` or `assistant`.
215    #[serde(default)]
216    pub role: Option<String>,
217    /// The model (assistant turns).
218    #[serde(default)]
219    pub model: Option<String>,
220    /// Content: a plain string or an array of blocks.
221    #[serde(default)]
222    pub content: MessageContent,
223    /// Anything else (usage, stop_reason, id, …).
224    #[serde(flatten)]
225    pub extra: ExtraFields,
226}
227
228/// A message body: plain text or a list of content blocks.
229#[derive(Debug, Clone, Deserialize)]
230#[serde(untagged)]
231pub enum MessageContent {
232    /// Plain string content.
233    Text(String),
234    /// Structured content blocks.
235    Blocks(Vec<super::ContentBlock>),
236}
237
238impl Default for MessageContent {
239    fn default() -> Self {
240        MessageContent::Blocks(Vec::new())
241    }
242}