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}