use super::*;
impl Session {
pub fn from_claude_code(path: impl AsRef<Path>) -> Result<Session> {
Self::from_claude_code_with_fidelity(path, Fidelity::ByteLossless)
}
pub fn from_claude_code_with_fidelity(
path: impl AsRef<Path>,
fidelity: Fidelity,
) -> Result<Session> {
let text = std::fs::read_to_string(path.as_ref())?;
let mut session = Self::from_claude_code_str_with_fidelity(&text, fidelity)?;
session.attach_claude_subagents(path.as_ref(), &text, fidelity)?;
Ok(session)
}
pub(super) fn attach_claude_subagents(
&mut self,
main_path: &Path,
main_text: &str,
fidelity: Fidelity,
) -> Result<()> {
let Some(dir) = subagents_dir_for(main_path) else {
return Ok(());
};
let entries = std::fs::read_dir(&dir).map_err(|error| {
crate::Error::Other(format!(
"failed to enumerate Claude subagents at {}: {error}",
dir.display()
))
})?;
let mut files = Vec::new();
for entry in entries {
let entry = entry.map_err(|error| {
crate::Error::Other(format!(
"failed to enumerate Claude subagents at {}: {error}",
dir.display()
))
})?;
let path = entry.path();
if path.extension().and_then(|extension| extension.to_str()) == Some("jsonl") {
files.push(path);
}
}
files.sort();
let mut collected: Vec<(Session, Option<String>)> = Vec::with_capacity(files.len());
for file in files {
let text = read_utf8_or_diagnose(&file).map_err(|error| {
crate::Error::Other(format!(
"failed to read Claude subagent {}: {error}",
file.display()
))
})?;
let sub = match Self::from_claude_code_str_with_fidelity(&text, fidelity) {
Ok(sub) => sub,
Err(error) if fidelity.tolerates_residue() => {
self.load_residue.push(format!(
"Claude subagent {} could not be reconstructed ({error}); it was omitted from this view",
file.display()
));
continue;
}
Err(error) => {
return Err(crate::Error::Other(format!(
"failed to reconstruct Claude subagent {}: {error}",
file.display()
)))
}
};
let agent_id = first_agent_id(&text).or_else(|| {
file.file_stem()
.and_then(|s| s.to_str())
.map(|s| s.trim_start_matches("agent-").to_string())
});
collected.push((sub, agent_id));
}
let agent_ids: Vec<String> = collected.iter().filter_map(|(_, id)| id.clone()).collect();
let index = parent_tool_use_index(main_text, &agent_ids);
for (mut sub, agent_id) in collected {
sub.meta.parent_tool_use_id = agent_id.as_ref().and_then(|id| index.get(id).cloned());
sub.meta.agent_id = agent_id;
self.subagents.push(sub);
}
Ok(())
}
pub fn from_claude_code_str(jsonl: &str) -> Result<Session> {
Self::from_claude_code_str_with_fidelity(jsonl, Fidelity::ByteLossless)
}
pub fn from_claude_code_str_with_fidelity(jsonl: &str, fidelity: Fidelity) -> Result<Session> {
let mut meta = SessionMeta::new(SessionSource::ClaudeCode);
let mut messages = Vec::new();
let (raw_lines, raw_trailing_newline) = split_lines_verbatim(jsonl);
let raw: Vec<String> = raw_lines.iter().map(|l| l.to_string()).collect();
let mut parse_error_lines = 0usize;
let mut index = ClaudeReplayIndex::default();
for (line_index, line) in raw_lines.iter().enumerate() {
if line.trim().is_empty() {
continue;
}
let v: Value = match serde_json::from_str(line) {
Ok(v) => v,
Err(_) => {
parse_error_lines += 1; continue;
}
};
capture_claude_meta(&v, &mut meta, line)?;
index.observe(line_index, &v)?;
}
let ClaudeReplaySelection {
lines: replay_lines,
residue: load_residue,
} = index.select_lines(fidelity)?;
let mut pending_assistant: Option<Value> = None;
for (record_index, line) in raw_lines.iter().enumerate() {
if let Ok(record) = serde_json::from_str::<Value>(line) {
capture_claude_residue(&mut meta, record_index, line, &record);
}
}
for line_index in replay_lines {
let line = raw_lines[line_index];
let v: Value = serde_json::from_str(line).map_err(crate::Error::Decode)?;
if v.get("type").and_then(Value::as_str) == Some("assistant") {
if v.get("isApiErrorMessage").and_then(Value::as_bool) == Some(true) {
flush_claude_assistant(&mut pending_assistant, &mut messages);
continue;
}
if let Some(pending) = pending_assistant.as_mut() {
if claude_assistant_message_id(pending).is_some_and(|message_id| {
claude_assistant_message_id(&v) == Some(message_id)
}) {
merge_claude_assistant_chunk(pending, &v);
continue;
}
flush_claude_assistant(&mut pending_assistant, &mut messages);
}
pending_assistant = Some(v);
continue;
}
flush_claude_assistant(&mut pending_assistant, &mut messages);
let before = messages.len();
match v.get("type").and_then(Value::as_str) {
Some("user") => push_claude_user(&v, &mut messages),
Some("assistant") => push_claude_assistant(&v, &mut messages),
Some("attachment") => push_claude_attachment(&v, &mut messages),
Some("system") => push_claude_system(&v, &mut messages),
_ => {} }
capture_claude_record_provenance(&v, &mut messages[before..]);
restore_single_grok_message(&v, &mut messages[before..]);
}
flush_claude_assistant(&mut pending_assistant, &mut messages);
reorder_tool_results_after_calls(&mut messages);
ensure_tool_results_paired(&mut messages);
let imported_message_count = Some(messages.len());
Ok(Session {
meta,
messages,
subagents: Vec::new(),
raw,
raw_trailing_newline,
imported_message_count,
raw_is_verbatim: true,
parse_error_lines,
load_residue,
})
}
}
fn subagents_dir_for(main_path: &Path) -> Option<PathBuf> {
let dir = main_path.parent()?;
let stem = main_path.file_stem()?.to_str()?;
let candidate = dir.join(stem).join("subagents");
candidate.is_dir().then_some(candidate)
}
fn first_agent_id(jsonl: &str) -> Option<String> {
for line in non_empty_lines(jsonl) {
if let Ok(v) = serde_json::from_str::<Value>(line) {
if let Some(id) = v.get("agentId").and_then(Value::as_str) {
return Some(id.to_string());
}
}
}
None
}
pub(super) fn parent_tool_use_index(
main_text: &str,
agent_ids: &[String],
) -> HashMap<String, String> {
let mut index: HashMap<String, String> = HashMap::new();
if agent_ids.is_empty() {
return index;
}
for line in non_empty_lines(main_text) {
if index.len() == agent_ids.len() {
break;
}
if !line.contains("tool_result") {
continue;
}
let still_unmapped: Vec<&String> = agent_ids
.iter()
.filter(|id| !index.contains_key(id.as_str()))
.collect();
if still_unmapped.is_empty() {
break;
}
let Ok(v) = serde_json::from_str::<Value>(line) else {
continue;
};
let content = v.get("message").and_then(|m| m.get("content"));
let Some(Value::Array(blocks)) = content else {
continue;
};
for b in blocks {
if b.get("type").and_then(Value::as_str) != Some("tool_result") {
continue;
}
let Some(tool_use_id) = b.get("tool_use_id").and_then(Value::as_str) else {
continue;
};
let block_str = b.to_string();
for id in &still_unmapped {
if index.contains_key(id.as_str()) {
continue;
}
if line.contains(id.as_str()) && block_str.contains(id.as_str()) {
index.insert((*id).clone(), tool_use_id.to_string());
}
}
}
}
index
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ClaudeReplayKind {
User,
Assistant,
Attachment,
System,
}
impl ClaudeReplayKind {
fn is_conversation(self) -> bool {
matches!(self, Self::User | Self::Assistant)
}
}
#[derive(Debug, Clone)]
struct ClaudeReplayNode {
line_index: usize,
uuid: String,
parent_uuid: Option<String>,
kind: ClaudeReplayKind,
is_sidechain: bool,
assistant_message_id: Option<String>,
is_tool_result: bool,
compact: Option<ClaudeCompactBoundary>,
}
#[derive(Debug, Clone)]
struct ClaudeCompactBoundary {
anchor_uuid: Option<String>,
preserved_uuids: Vec<String>,
preserved_segment: Option<(String, String)>,
}
#[derive(Debug, Default)]
struct ClaudeReplaySelection {
lines: Vec<usize>,
residue: Vec<String>,
}
#[derive(Debug, Default)]
struct ClaudeReplayIndex {
nodes: Vec<ClaudeReplayNode>,
by_uuid: HashMap<String, usize>,
segment_anchors: HashSet<String>,
last_prompt: Option<(String, bool)>,
linear_lines: Vec<usize>,
}
impl ClaudeReplayIndex {
fn observe(&mut self, line_index: usize, v: &Value) -> Result<()> {
if v.get("type").and_then(Value::as_str) == Some("last-prompt") {
if let Some(leaf) = v.get("leafUuid").and_then(Value::as_str) {
self.last_prompt = Some((
leaf.to_string(),
v.get("explicit").and_then(Value::as_bool) == Some(true),
));
}
return Ok(());
}
if v.get("type").and_then(Value::as_str) == Some("fork-context-ref") {
if let Some(uuid) = v.get("uuid").and_then(Value::as_str) {
self.segment_anchors.insert(uuid.to_string());
}
return Ok(());
}
let kind = match v.get("type").and_then(Value::as_str) {
Some("user") => ClaudeReplayKind::User,
Some("assistant") => ClaudeReplayKind::Assistant,
Some("attachment") => ClaudeReplayKind::Attachment,
Some("system") => ClaudeReplayKind::System,
_ => return Ok(()),
};
self.linear_lines.push(line_index);
let Some(uuid) = v.get("uuid").and_then(Value::as_str) else {
return Ok(());
};
if self.by_uuid.contains_key(uuid) {
return Err(claude_replay_error(format!(
"duplicate uuid `{uuid}` in Claude transcript"
)));
}
let compact = (kind == ClaudeReplayKind::System
&& v.get("subtype").and_then(Value::as_str) == Some("compact_boundary"))
.then(|| ClaudeCompactBoundary::from_value(v));
let assistant_message_id = (kind == ClaudeReplayKind::Assistant)
.then(|| claude_assistant_message_id(v).map(str::to_string))
.flatten();
let is_tool_result = kind == ClaudeReplayKind::User
&& v.get("message")
.and_then(|m| m.get("content"))
.and_then(Value::as_array)
.is_some_and(|blocks| {
blocks
.iter()
.any(|b| b.get("type").and_then(Value::as_str) == Some("tool_result"))
});
let node = ClaudeReplayNode {
line_index,
uuid: uuid.to_string(),
parent_uuid: v
.get("parentUuid")
.and_then(Value::as_str)
.map(str::to_string),
kind,
is_sidechain: v.get("isSidechain").and_then(Value::as_bool) == Some(true),
assistant_message_id,
is_tool_result,
compact,
};
self.by_uuid.insert(uuid.to_string(), self.nodes.len());
self.nodes.push(node);
Ok(())
}
fn select_lines(mut self, fidelity: Fidelity) -> Result<ClaudeReplaySelection> {
let lenient = fidelity.tolerates_residue();
let mut residue = Vec::new();
if self.nodes.is_empty() {
return Ok(ClaudeReplaySelection {
lines: self.linear_lines,
residue,
});
}
if lenient {
self.anchor_dangling_parents(&mut residue);
}
let fallback = lenient.then(|| self.linear_lines.clone());
match self.project(lenient, &mut residue) {
Ok(lines) => Ok(ClaudeReplaySelection { lines, residue }),
Err(error) => match fallback {
Some(lines) => {
residue.push(format!(
"the Claude record graph could not be projected ({error}); \
every record was stitched in transcript order instead"
));
Ok(ClaudeReplaySelection { lines, residue })
}
None => Err(error),
},
}
}
fn anchor_dangling_parents(&mut self, residue: &mut Vec<String>) {
let mut dangling = Vec::new();
for idx in 0..self.nodes.len() {
let Some(parent) = self.nodes[idx].parent_uuid.as_deref() else {
continue;
};
if self.by_uuid.contains_key(parent) || self.segment_anchors.contains(parent) {
continue;
}
dangling.push(format!("`{}` → `{parent}`", self.nodes[idx].uuid));
self.nodes[idx].parent_uuid = None;
}
if dangling.is_empty() {
return;
}
const NAMED: usize = 8;
let total = dangling.len();
let overflow = total.saturating_sub(NAMED);
dangling.truncate(NAMED);
let mut listed = dangling.join(", ");
if overflow > 0 {
listed.push_str(&format!(", and {overflow} more"));
}
residue.push(format!(
"{total} Claude record(s) reference a parentUuid absent from the transcript and were \
anchored as segment roots: {listed}"
));
}
fn project(&mut self, lenient: bool, residue: &mut Vec<String>) -> Result<Vec<usize>> {
let mut retained = vec![true; self.nodes.len()];
if let Some(boundary_index) = self.nodes.iter().rposition(|n| n.compact.is_some()) {
let parents: Option<Vec<Option<String>>> = lenient.then(|| {
self.nodes
.iter()
.map(|node| node.parent_uuid.clone())
.collect()
});
if let Err(error) = self.apply_latest_compaction(boundary_index, &mut retained) {
let Some(parents) = parents else {
return Err(error);
};
for (node, parent) in self.nodes.iter_mut().zip(parents) {
node.parent_uuid = parent;
}
retained.iter_mut().for_each(|keep| *keep = true);
residue.push(format!(
"the latest Claude compact boundary could not be projected ({error}); \
no pre-compaction record was pruned from this view"
));
}
}
let sidechain_only = self
.nodes
.iter()
.enumerate()
.filter(|(idx, node)| retained[*idx] && node.kind.is_conversation())
.all(|(_, node)| node.is_sidechain);
let explicit_leaf = self
.last_prompt
.as_ref()
.filter(|(_, explicit)| *explicit)
.and_then(|(uuid, _)| self.by_uuid.get(uuid).copied())
.filter(|idx| retained[*idx]);
let newest_non_sidechain = self
.nodes
.iter()
.enumerate()
.rev()
.find(|(idx, node)| retained[*idx] && !node.is_sidechain)
.map(|(idx, _)| idx);
let newest_sidechain = self
.nodes
.iter()
.enumerate()
.rev()
.find(|(idx, node)| retained[*idx] && node.is_sidechain && node.kind.is_conversation())
.map(|(idx, _)| idx);
let mut active = explicit_leaf
.or(newest_non_sidechain)
.or(newest_sidechain)
.ok_or_else(|| claude_replay_error("no Claude record remains after compaction"))?;
let mut seeking = HashSet::new();
while !self.nodes[active].kind.is_conversation() {
if !seeking.insert(active) {
return Err(claude_replay_error(
"cycle while resolving active Claude leaf",
));
}
active = self.parent_index(active, &retained)?;
}
let mut segments =
vec![self.project_segment(active, &retained, sidechain_only, lenient)?];
if lenient {
for leaf in self.severed_segment_leaves(active, &retained) {
segments.push(self.project_segment(leaf, &retained, sidechain_only, lenient)?);
}
if segments.len() > 1 {
residue.push(format!(
"{} conversation segments were stitched in transcript order because the \
Claude record graph is severed",
segments.len()
));
}
}
segments.retain(|segment| !segment.is_empty());
segments.sort_by_key(|segment| {
segment
.iter()
.map(|idx| self.nodes[*idx].line_index)
.min()
.unwrap_or(usize::MAX)
});
let mut ordered = Vec::new();
let mut placed = HashSet::new();
for idx in segments.into_iter().flatten() {
if placed.insert(idx) {
ordered.push(idx);
}
}
self.recover_parallel_assistant_chunks(ordered, &retained)
.map(|indices| {
indices
.into_iter()
.map(|idx| self.nodes[idx].line_index)
.collect()
})
}
fn project_segment(
&self,
leaf: usize,
retained: &[bool],
sidechain_only: bool,
lenient: bool,
) -> Result<Vec<usize>> {
let mut reversed = Vec::new();
let mut seen = HashSet::new();
let mut cursor = Some(leaf);
while let Some(idx) = cursor {
if !seen.insert(idx) {
return Err(claude_replay_error(format!(
"cycle in active Claude parentUuid chain at `{}`",
self.nodes[idx].uuid
)));
}
reversed.push(idx);
cursor = match self.nodes[idx].parent_uuid.as_deref() {
Some(parent) => match self.by_uuid.get(parent).copied() {
Some(parent) => Some(parent),
None if self.segment_anchors.contains(parent) => None,
None if sidechain_only => None,
None => {
return Err(claude_replay_error(format!(
"active Claude record `{}` has missing parentUuid `{parent}`",
self.nodes[idx].uuid
)));
}
},
None => None,
};
if cursor.is_some_and(|parent| !retained[parent]) {
if lenient {
break;
}
return Err(claude_replay_error(format!(
"active Claude chain crosses an excluded compaction record from `{}`",
self.nodes[idx].uuid
)));
}
}
reversed.reverse();
let mut descendants = Vec::new();
let mut frontier = vec![leaf];
let mut head = 0;
while head < frontier.len() {
let parent = frontier[head];
head += 1;
for (idx, node) in self.nodes.iter().enumerate() {
if !retained[idx]
|| node.kind.is_conversation()
|| seen.contains(&idx)
|| node.parent_uuid.as_deref() != Some(self.nodes[parent].uuid.as_str())
{
continue;
}
seen.insert(idx);
descendants.push(idx);
frontier.push(idx);
}
}
descendants.sort_by_key(|idx| self.nodes[*idx].line_index);
reversed.extend(descendants);
Ok(reversed)
}
fn severed_segment_leaves(&self, active: usize, retained: &[bool]) -> Vec<usize> {
let active_root = self.component_root(active, retained);
let mut newest_by_root: BTreeMap<usize, usize> = BTreeMap::new();
for idx in 0..self.nodes.len() {
if !retained[idx] || !self.nodes[idx].kind.is_conversation() {
continue;
}
let Some(root) = self.component_root(idx, retained) else {
continue;
};
if Some(root) == active_root {
continue;
}
let newest = newest_by_root.entry(root).or_insert(idx);
if self.nodes[idx].line_index > self.nodes[*newest].line_index {
*newest = idx;
}
}
newest_by_root.into_values().collect()
}
fn component_root(&self, idx: usize, retained: &[bool]) -> Option<usize> {
let mut cursor = idx;
let mut seen = HashSet::new();
loop {
if !seen.insert(cursor) {
return None;
}
let next = self.nodes[cursor]
.parent_uuid
.as_deref()
.and_then(|parent| self.by_uuid.get(parent).copied())
.filter(|parent| retained[*parent]);
match next {
Some(parent) => cursor = parent,
None => return Some(cursor),
}
}
}
fn apply_latest_compaction(
&mut self,
boundary_index: usize,
retained: &mut [bool],
) -> Result<()> {
let compact = self.nodes[boundary_index]
.compact
.clone()
.expect("called with compact boundary");
let mut preserved = compact.preserved_uuids;
if preserved.is_empty() {
if let Some((head, tail)) = compact.preserved_segment {
preserved = self.walk_preserved_segment(&head, &tail)?;
}
}
let preserved_set: HashSet<String> = preserved.iter().cloned().collect();
for uuid in &preserved {
if !self.by_uuid.contains_key(uuid) {
return Err(claude_replay_error(format!(
"latest compact boundary references missing preserved uuid `{uuid}`"
)));
}
}
let removed_uuids: HashSet<String> = self
.nodes
.iter()
.enumerate()
.filter(|(idx, node)| *idx < boundary_index && !preserved_set.contains(&node.uuid))
.map(|(_, node)| node.uuid.clone())
.collect();
for (idx, node) in self.nodes.iter().enumerate() {
if idx < boundary_index && !preserved_set.contains(&node.uuid) {
retained[idx] = false;
}
}
if preserved.is_empty() {
return Ok(());
}
let anchor = compact.anchor_uuid.ok_or_else(|| {
claude_replay_error("preserved compact boundary is missing anchorUuid")
})?;
if !self.by_uuid.contains_key(&anchor) {
return Err(claude_replay_error(format!(
"latest compact boundary references missing anchor uuid `{anchor}`"
)));
}
let tail = preserved.last().cloned().expect("non-empty preserved list");
let mut parent = anchor.clone();
for uuid in &preserved {
let idx = self.by_uuid[uuid];
self.nodes[idx].parent_uuid = Some(parent);
parent = uuid.clone();
}
let first = &preserved[0];
for node in &mut self.nodes {
if node.parent_uuid.as_deref() == Some(anchor.as_str()) && node.uuid != *first {
node.parent_uuid = Some(tail.clone());
}
}
for node in &mut self.nodes {
if node.kind.is_conversation()
&& node
.parent_uuid
.as_ref()
.is_some_and(|parent| removed_uuids.contains(parent))
{
node.parent_uuid = Some(tail.clone());
}
}
Ok(())
}
fn walk_preserved_segment(&self, head: &str, tail: &str) -> Result<Vec<String>> {
let mut reversed = Vec::new();
let mut seen = HashSet::new();
let mut cursor = tail;
loop {
if !seen.insert(cursor.to_string()) {
return Err(claude_replay_error("cycle in compact preservedSegment"));
}
let idx = *self.by_uuid.get(cursor).ok_or_else(|| {
claude_replay_error(format!(
"compact preservedSegment references missing uuid `{cursor}`"
))
})?;
reversed.push(cursor.to_string());
if cursor == head {
reversed.reverse();
return Ok(reversed);
}
cursor = self.nodes[idx].parent_uuid.as_deref().ok_or_else(|| {
claude_replay_error(format!(
"compact preservedSegment tail `{tail}` does not reach head `{head}`"
))
})?;
}
}
fn parent_index(&self, idx: usize, retained: &[bool]) -> Result<usize> {
let parent = self.nodes[idx].parent_uuid.as_deref().ok_or_else(|| {
claude_replay_error(format!(
"Claude record `{}` has no conversational ancestor",
self.nodes[idx].uuid
))
})?;
let parent_idx = *self.by_uuid.get(parent).ok_or_else(|| {
claude_replay_error(format!(
"Claude record `{}` has missing parentUuid `{parent}`",
self.nodes[idx].uuid
))
})?;
if !retained[parent_idx] {
return Err(claude_replay_error(format!(
"Claude record `{}` points into compacted-out history",
self.nodes[idx].uuid
)));
}
Ok(parent_idx)
}
fn recover_parallel_assistant_chunks(
&self,
base: Vec<usize>,
retained: &[bool],
) -> Result<Vec<usize>> {
let selected: HashSet<usize> = base.iter().copied().collect();
let mut replacements: HashMap<usize, Vec<usize>> = HashMap::new();
let mut skipped_positions = HashSet::new();
let mut handled_ids = HashSet::new();
for (base_pos, idx) in base.iter().copied().enumerate() {
let Some(message_id) = self.nodes[idx].assistant_message_id.as_deref() else {
continue;
};
if !handled_ids.insert(message_id.to_string()) {
continue;
}
let base_positions: Vec<usize> = base
.iter()
.enumerate()
.filter(|(_, candidate)| {
self.nodes[**candidate].assistant_message_id.as_deref() == Some(message_id)
})
.map(|(pos, _)| pos)
.collect();
let anchor_pos = base_positions.first().copied().unwrap_or(base_pos);
skipped_positions.extend(base_positions.iter().copied().skip(1));
let mut chunks: Vec<usize> = self
.nodes
.iter()
.enumerate()
.filter(|(candidate, node)| {
retained[*candidate] && node.assistant_message_id.as_deref() == Some(message_id)
})
.map(|(candidate, _)| candidate)
.collect();
chunks.sort_by_key(|candidate| self.nodes[*candidate].line_index);
let assistant_uuids: HashSet<&str> = self
.nodes
.iter()
.filter(|node| node.assistant_message_id.as_deref() == Some(message_id))
.map(|node| node.uuid.as_str())
.collect();
let mut results: Vec<usize> = self
.nodes
.iter()
.enumerate()
.filter(|(candidate, node)| {
retained[*candidate]
&& !selected.contains(candidate)
&& node.is_tool_result
&& node
.parent_uuid
.as_deref()
.is_some_and(|parent| assistant_uuids.contains(parent))
})
.map(|(candidate, _)| candidate)
.collect();
results.sort_by_key(|candidate| self.nodes[*candidate].line_index);
chunks.extend(results);
replacements.insert(anchor_pos, chunks);
}
let mut out = Vec::with_capacity(selected.len());
for (pos, idx) in base.into_iter().enumerate() {
if let Some(replacement) = replacements.remove(&pos) {
out.extend(replacement);
} else if !skipped_positions.contains(&pos) {
out.push(idx);
}
}
Ok(out)
}
}
impl ClaudeCompactBoundary {
fn from_value(v: &Value) -> Self {
let metadata = v.get("compactMetadata");
let preserved_messages = metadata.and_then(|m| m.get("preservedMessages"));
let anchor_uuid = preserved_messages
.and_then(|p| p.get("anchorUuid"))
.and_then(Value::as_str)
.or_else(|| {
metadata
.and_then(|m| m.get("preservedSegment"))
.and_then(|p| p.get("anchorUuid"))
.and_then(Value::as_str)
})
.map(str::to_string);
let preserved_uuids = preserved_messages
.and_then(|p| p.get("uuids"))
.and_then(Value::as_array)
.map(|uuids| {
uuids
.iter()
.filter_map(Value::as_str)
.map(str::to_string)
.collect()
})
.unwrap_or_default();
let preserved_segment =
metadata
.and_then(|m| m.get("preservedSegment"))
.and_then(|segment| {
Some((
segment.get("headUuid")?.as_str()?.to_string(),
segment.get("tailUuid")?.as_str()?.to_string(),
))
});
Self {
anchor_uuid,
preserved_uuids,
preserved_segment,
}
}
}
fn claude_replay_error(message: impl Into<String>) -> crate::Error {
crate::Error::Other(format!(
"cannot reconstruct lossless Claude continuation: {}",
message.into()
))
}
fn claude_assistant_message_id(v: &Value) -> Option<&str> {
v.get("message")
.and_then(|message| message.get("id"))
.and_then(Value::as_str)
}
fn merge_claude_assistant_chunk(target: &mut Value, chunk: &Value) {
let Some(target_message) = target.get_mut("message") else {
return;
};
let Some(chunk_message) = chunk.get("message") else {
return;
};
let mut content = target_message
.get("content")
.and_then(Value::as_array)
.cloned()
.unwrap_or_default();
if let Some(blocks) = chunk_message.get("content").and_then(Value::as_array) {
content.extend(blocks.iter().cloned());
}
let mut merged_message = chunk_message.clone();
merged_message["content"] = Value::Array(content);
*target_message = merged_message;
}
fn flush_claude_assistant(pending: &mut Option<Value>, out: &mut Vec<ChatMessage>) {
let Some(v) = pending.take() else {
return;
};
let reasoning_only = claude_assistant_message_id(&v).is_some()
&& v.get("message")
.and_then(|message| message.get("content"))
.and_then(Value::as_array)
.is_some_and(|blocks| {
!blocks.is_empty()
&& blocks.iter().all(|block| {
matches!(
block.get("type").and_then(Value::as_str),
Some("thinking" | "redacted_thinking")
)
})
});
if reasoning_only {
return;
}
let before = out.len();
push_claude_assistant(&v, out);
capture_claude_record_provenance(&v, &mut out[before..]);
restore_single_grok_message(&v, &mut out[before..]);
}
fn capture_claude_record_provenance(v: &Value, messages: &mut [ChatMessage]) {
let timestamp = v.get("timestamp").and_then(Value::as_str);
let uuid = v.get("uuid").and_then(Value::as_str);
let model = v
.get("message")
.and_then(|message| message.get("model"))
.and_then(Value::as_str);
for message in messages {
if let Some(timestamp) = timestamp {
message
.metadata
.entry("timestamp".to_string())
.or_insert_with(|| timestamp.to_string());
}
if let Some(uuid) = uuid {
message
.metadata
.entry("claude_uuid".to_string())
.or_insert_with(|| uuid.to_string());
}
if let Some(model) = model {
message
.metadata
.entry("model".to_string())
.or_insert_with(|| model.to_string());
}
}
}
fn capture_claude_meta(v: &Value, meta: &mut SessionMeta, raw_line: &str) -> Result<()> {
restore_codex_provenance_from_top_level(v, meta)?;
if meta.session_id.is_none() {
if let Some(id) = v.get("sessionId").and_then(Value::as_str) {
meta.session_id = Some(id.to_string());
}
}
if meta.cwd.is_none() {
if let Some(cwd) = v.get("cwd").and_then(Value::as_str) {
meta.cwd = Some(PathBuf::from(cwd));
}
}
if meta.model.is_none() {
if let Some(model) = v
.get("message")
.and_then(|m| m.get("model"))
.and_then(Value::as_str)
{
meta.model = Some(model.to_string());
}
}
if v.get("type").and_then(Value::as_str) == Some("fork-context-ref")
&& !meta.lineage.contains_key("claude_fork_context_ref_raw")
{
meta.lineage.insert(
"claude_fork_context_ref_raw".to_string(),
raw_line.to_string(),
);
}
Ok(())
}
fn push_claude_user(v: &Value, out: &mut Vec<ChatMessage>) {
let content = v.get("message").and_then(|m| m.get("content"));
let provenance = claude_user_provenance(v);
match content {
Some(Value::String(s)) => {
if !s.trim().is_empty() {
out.push(ChatMessage::user(s.clone()).with_metas(&provenance));
}
}
Some(Value::Array(blocks)) => {
let mut text = String::new();
let mut images: Vec<Value> = Vec::new();
let mut saw_unconvertible_image = false;
for b in blocks {
match b.get("type").and_then(Value::as_str) {
Some("text") => push_text(&mut text, b.get("text")),
Some("tool_result") => {
let id = b
.get("tool_use_id")
.and_then(Value::as_str)
.unwrap_or_default();
let (result, images) =
extract_tool_result_content(b.get("content"), v.get("toolUseResult"));
let mut msg = tool_message(id, result);
if !images.is_empty() {
let mut parts = Vec::new();
if let Some(t) = &msg.content {
if !t.is_empty() {
parts.push(serde_json::json!({"type": "text", "text": t}));
}
}
parts.extend(images);
msg.content_parts = Some(parts);
}
if let Some(src) = v.get("sourceToolAssistantUUID").and_then(Value::as_str)
{
msg.metadata
.insert("sourceToolAssistantUUID".to_string(), src.to_string());
}
if b.get("is_error").and_then(Value::as_bool) == Some(true) {
crate::mark_tool_error(&mut msg);
} else {
restore_tool_outcome_extension(v, &mut msg);
}
out.push(msg);
}
Some("image") => match claude_image_block_to_part(b) {
Some(part) => images.push(part),
None => saw_unconvertible_image = true,
},
_ => {} }
}
if images.is_empty() && text.trim().is_empty() && saw_unconvertible_image {
push_str_field(&mut text, UNCONVERTIBLE_IMAGE_MARKER);
}
let before = out.len();
if !images.is_empty() {
let mut parts = Vec::new();
if !text.trim().is_empty() {
parts.push(serde_json::json!({"type": "text", "text": text.clone()}));
}
parts.extend(images);
out.push(
ChatMessage {
role: Role::User,
content: None,
content_parts: Some(parts),
tool_calls: None,
tool_call_id: None,
name: None,
metadata: Default::default(),
}
.with_metas(&provenance),
);
} else if !text.trim().is_empty() {
out.push(ChatMessage::user(text).with_metas(&provenance));
}
if saw_unconvertible_image && out.len() > before {
if let Some(msg) = out.last_mut() {
msg.metadata
.insert("image_source_unconvertible".to_string(), "true".to_string());
}
}
}
_ => {}
}
}
pub(super) const UNCONVERTIBLE_IMAGE_MARKER: &str =
"[image: source not captured — unsupported/unconvertible image reference]";
pub(super) fn claude_image_block_to_part(b: &Value) -> Option<Value> {
let source = b.get("source")?;
match source.get("type").and_then(Value::as_str) {
Some("base64") => {
let mime = source.get("media_type").and_then(Value::as_str)?;
let data = source.get("data").and_then(Value::as_str)?;
if mime.is_empty() || data.is_empty() {
return None;
}
Some(serde_json::json!({
"type": "image_url",
"image_url": {"url": format!("data:{mime};base64,{data}")},
}))
}
Some("url") => {
let url = source.get("url").and_then(Value::as_str)?;
if url.is_empty() {
return None;
}
Some(serde_json::json!({"type": "image_url", "image_url": {"url": url}}))
}
_ => None,
}
}
fn claude_user_content_value(msg: &ChatMessage) -> Value {
match &msg.content_parts {
Some(parts) => {
let mut blocks = Vec::new();
for p in parts {
match p.get("type").and_then(Value::as_str) {
Some("text") => {
if let Some(t) = p.get("text").and_then(Value::as_str) {
if !t.is_empty() {
blocks.push(serde_json::json!({"type": "text", "text": t}));
}
}
}
Some("image_url") => {
if let Some(url) = p
.get("image_url")
.and_then(|u| u.get("url"))
.and_then(Value::as_str)
{
blocks.push(match parse_data_uri(url) {
Some((mime, data)) => serde_json::json!({
"type": "image",
"source": {"type": "base64", "media_type": mime, "data": data},
}),
None => serde_json::json!({
"type": "image",
"source": {"type": "url", "url": url},
}),
});
}
}
_ => {}
}
}
Value::Array(blocks)
}
None => Value::String(msg.content.clone().unwrap_or_default()),
}
}
fn claude_tool_result_content_value(msg: &ChatMessage) -> Value {
match &msg.content_parts {
Some(parts) if !parts.is_empty() => {
let mut blocks = Vec::new();
if let Some(t) = &msg.content {
if !t.is_empty() {
blocks.push(serde_json::json!({"type": "text", "text": t}));
}
}
for p in parts {
if p.get("type").and_then(Value::as_str) == Some("image_url") {
if let Some(url) = p
.get("image_url")
.and_then(|u| u.get("url"))
.and_then(Value::as_str)
{
blocks.push(match parse_data_uri(url) {
Some((mime, data)) => serde_json::json!({
"type": "image",
"source": {"type": "base64", "media_type": mime, "data": data},
}),
None => serde_json::json!({
"type": "image",
"source": {"type": "url", "url": url},
}),
});
}
}
}
Value::Array(blocks)
}
_ => Value::String(msg.content.clone().unwrap_or_default()),
}
}
pub(crate) fn claude_user_provenance(v: &Value) -> Vec<(String, String)> {
let mut out = Vec::new();
let mut take_str = |key: &str| {
if let Some(s) = v.get(key).and_then(Value::as_str) {
out.push((key.to_string(), s.to_string()));
}
};
take_str("promptSource"); take_str("interruptedMessageId");
take_str("sourceToolUseID");
for flag in ["isMeta", "isCompactSummary", "isVisibleInTranscriptOnly"] {
if v.get(flag).and_then(Value::as_bool) == Some(true) {
out.push((flag.to_string(), "true".to_string()));
}
}
if let Some(n) = v.get("queuePriority").and_then(Value::as_i64) {
out.push(("queuePriority".to_string(), n.to_string()));
}
if let Some(kind) = v
.get("origin")
.and_then(|o| o.get("kind"))
.and_then(Value::as_str)
{
out.push(("origin".to_string(), kind.to_string()));
}
out
}
fn push_claude_system(v: &Value, out: &mut Vec<ChatMessage>) {
let keep = matches!(
v.get("subtype").and_then(Value::as_str),
Some("scheduled_task_fire") | Some("local_command") | Some("away_summary")
);
if !keep {
return;
}
if let Some(content) = v.get("content").and_then(Value::as_str) {
if !content.trim().is_empty() {
let subtype = v.get("subtype").and_then(Value::as_str).unwrap_or("system");
out.push(ChatMessage::system(content.to_string()).with_meta("systemSubtype", subtype));
}
}
}
fn push_claude_attachment(v: &Value, out: &mut Vec<ChatMessage>) {
let att = match v.get("attachment") {
Some(a) => a,
None => return,
};
let kind = match att.get("type").and_then(Value::as_str) {
Some(kind) => kind,
None => return,
};
let text = match kind {
"queued_command" => att
.get("prompt")
.and_then(Value::as_str)
.map(str::to_string),
"file" => attachment_with_path(att, "attached file", "filename", "content"),
"edited_text_file" => attachment_with_path(att, "edited file", "filename", "snippet"),
"nested_memory" => attachment_with_path(att, "project memory", "path", "content"),
_ => None, };
let Some(text) = text else { return };
if text.trim().is_empty() {
return;
}
let mut message = ChatMessage::user(text).with_meta("attachmentType", kind);
if let Some(mode) = att.get("commandMode").and_then(Value::as_str) {
message = message.with_meta("commandMode", mode);
}
out.push(message);
}
fn attachment_with_path(
att: &Value,
label: &str,
path_key: &str,
body_key: &str,
) -> Option<String> {
let body = att.get(body_key).and_then(Value::as_str)?;
let path = att
.get(path_key)
.or_else(|| att.get("displayPath"))
.and_then(Value::as_str)
.unwrap_or("");
Some(format!("[{label}: {path}]\n{body}"))
}
pub(super) fn push_str_field(buf: &mut String, s: &str) {
if !buf.is_empty() {
buf.push('\n');
}
buf.push_str(s);
}
pub(super) fn orphaned_reasoning_message(
reasoning: &mut String,
reasoning_content: &mut String,
encrypted: &mut bool,
) -> ChatMessage {
let mut msg = ChatMessage::system("[reasoning] (turn ended without a reply)".to_string());
if !reasoning.is_empty() {
msg = msg.with_meta("reasoning", std::mem::take(reasoning));
}
if !reasoning_content.is_empty() {
msg = msg.with_meta("reasoning_content", std::mem::take(reasoning_content));
}
if *encrypted {
msg = msg.with_meta("reasoning_encrypted", "true");
*encrypted = false;
}
msg
}
fn push_claude_assistant(v: &Value, out: &mut Vec<ChatMessage>) {
let content = v.get("message").and_then(|m| m.get("content"));
let mut text = String::new();
let mut calls: Vec<ToolCall> = Vec::new();
let mut thinking = String::new();
let mut signature: Option<String> = None;
let mut redacted_thinking: Option<String> = None;
let mut redacted_thinking_seen = false;
let mut images: Vec<Value> = Vec::new();
let mut thinking_block_seen = false;
let mut thinking_blocks: Vec<Value> = Vec::new();
let mut saw_unconvertible_image = false;
match content {
Some(Value::String(s)) => push_text(&mut text, Some(&Value::String(s.clone()))),
Some(Value::Array(blocks)) => {
for b in blocks {
match b.get("type").and_then(Value::as_str) {
Some("text") => push_text(&mut text, b.get("text")),
Some("tool_use") => {
let id = b.get("id").and_then(Value::as_str).unwrap_or_default();
let name = b.get("name").and_then(Value::as_str).unwrap_or_default();
let args = b
.get("input")
.map(|i| i.to_string())
.unwrap_or_else(|| "{}".to_string());
calls.push(function_call(id, name, args));
}
Some("thinking") => {
thinking_block_seen = true;
let t = b.get("thinking").and_then(Value::as_str).unwrap_or("");
if !t.is_empty() {
push_str_field(&mut thinking, t); }
let sig = b.get("signature").and_then(Value::as_str);
if let Some(s) = sig {
signature = Some(s.to_string()); }
let mut block = serde_json::json!({"type": "thinking", "thinking": t});
if let Some(s) = sig {
block["signature"] = Value::String(s.to_string());
}
thinking_blocks.push(block);
}
Some("redacted_thinking") => {
redacted_thinking_seen = true;
let data = b.get("data").and_then(Value::as_str);
if let Some(d) = data {
redacted_thinking = Some(d.to_string()); }
let mut block = serde_json::json!({"type": "redacted_thinking"});
if let Some(d) = data {
block["data"] = Value::String(d.to_string());
}
thinking_blocks.push(block);
}
Some("image") => match claude_image_block_to_part(b) {
Some(part) => images.push(part),
None => saw_unconvertible_image = true,
},
Some("fallback") => {
let from = b
.get("from")
.and_then(|f| f.get("model"))
.and_then(Value::as_str)
.unwrap_or("?");
let to = b
.get("to")
.and_then(|t| t.get("model"))
.and_then(Value::as_str)
.unwrap_or("?");
push_str_field(&mut text, &format!("[model fallback: {from} -> {to}]"));
}
_ => {}
}
}
}
_ => {}
}
if images.is_empty() && text.trim().is_empty() && saw_unconvertible_image {
push_str_field(&mut text, UNCONVERTIBLE_IMAGE_MARKER);
}
let before = out.len();
if !images.is_empty() {
let mut parts = Vec::new();
if !text.trim().is_empty() {
parts.push(serde_json::json!({"type": "text", "text": text.clone()}));
}
parts.extend(images);
out.push(ChatMessage {
role: Role::Assistant,
content: None,
content_parts: Some(parts),
tool_calls: (!calls.is_empty()).then_some(calls),
tool_call_id: None,
name: None,
metadata: Default::default(),
});
} else {
push_assistant(out, text, calls);
if out.len() == before {
let mut empty = ChatMessage {
role: Role::Assistant,
content: None,
content_parts: None,
tool_calls: None,
tool_call_id: None,
name: None,
metadata: Default::default(),
};
if !thinking_block_seen && !redacted_thinking_seen {
empty
.metadata
.insert("empty_assistant_record".to_string(), "true".to_string());
}
out.push(empty);
}
}
if out.len() > before {
if let Some(msg) = out.last_mut() {
if thinking_block_seen {
msg.metadata.insert("thinking".to_string(), thinking);
}
if let Some(sig) = signature {
msg.metadata.insert("thinking_signature".to_string(), sig);
}
if let Some(rt) = redacted_thinking {
msg.metadata.insert("redacted_thinking".to_string(), rt);
}
if !thinking_blocks.is_empty() {
msg.metadata.insert(
"thinking_blocks".to_string(),
Value::Array(thinking_blocks).to_string(),
);
}
if saw_unconvertible_image {
msg.metadata
.insert("image_source_unconvertible".to_string(), "true".to_string());
}
for key in [
"attributionSkill",
"attributionAgent",
"attributionMcpServer",
"attributionMcpTool",
"slug",
] {
if let Some(s) = v.get(key).and_then(Value::as_str) {
msg.metadata.insert(key.to_string(), s.to_string());
}
}
}
}
}
pub(super) fn claude_residue_kind(record: &Value) -> Option<&'static str> {
match record.get("type").and_then(Value::as_str) {
Some("file-history-snapshot") => Some("file-history-snapshot"),
Some("queue-operation") => Some("queue-operation"),
Some("last-prompt") => Some("last-prompt"),
Some("mode") => Some("mode"),
Some("fork-context-ref") => Some("fork-context-ref"),
_ => None,
}
}
fn claude_residue_line_for_emit(raw: &str, session_id: &str, cwd: &str) -> String {
let Ok(mut value) = serde_json::from_str::<Value>(raw) else {
return raw.to_string();
};
let Some(object) = value.as_object_mut() else {
return raw.to_string();
};
let mut changed = false;
for (key, current) in [("sessionId", session_id), ("cwd", cwd)] {
if object
.get(key)
.and_then(Value::as_str)
.is_some_and(|existing| existing != current)
{
object.insert(key.to_string(), Value::String(current.to_string()));
changed = true;
}
}
if changed {
value.to_string()
} else {
raw.to_string()
}
}
fn capture_claude_residue(
meta: &mut SessionMeta,
record_index: usize,
raw_line: &str,
record: &Value,
) {
let Some(kind) = claude_residue_kind(record) else {
return;
};
capture_native_residue(meta, "claude_code", record_index, raw_line, record, kind);
}
fn next_claude_uuid(counter: &mut usize, used_ids: &mut HashSet<String>) -> String {
loop {
let candidate = synth_uuid(*counter);
*counter += 1;
if used_ids.insert(candidate.clone()) {
return candidate;
}
}
}
fn claude_message_uuid(
msg: &ChatMessage,
counter: &mut usize,
used_ids: &mut HashSet<String>,
) -> String {
for key in ["claude_uuid", "supercode_native_uuid"] {
if let Some(candidate) = msg.metadata.get(key) {
if !candidate.is_empty() && used_ids.insert(candidate.clone()) {
return candidate.clone();
}
}
}
next_claude_uuid(counter, used_ids)
}
fn collect_claude_uuids_from_raw(raw_prefix: &[String]) -> HashSet<String> {
let mut ids = HashSet::new();
for line in raw_prefix {
if let Ok(v) = serde_json::from_str::<Value>(line) {
if let Some(uuid) = v.get("uuid").and_then(Value::as_str) {
ids.insert(uuid.to_string());
}
}
}
ids
}
impl Session {
pub(super) fn to_claude_code_jsonl(&self) -> String {
let session_id = self
.meta
.session_id
.clone()
.unwrap_or_else(|| synth_uuid(0));
let cwd = self.cwd_string();
let mut out = String::new();
if let Some(raw) = self.meta.lineage.get("claude_fork_context_ref_raw") {
out.push_str(raw);
out.push('\n');
}
self.write_claude_code_records(
&mut out,
&self.messages,
&session_id,
&cwd,
None,
1,
&HashSet::new(),
);
if self.meta.native_residue_source.as_deref() == Some("claude_code") {
let mut records: Vec<&Value> = self.meta.native_residue.iter().collect();
records.sort_by_key(|entry| {
entry
.get("record_index")
.and_then(Value::as_u64)
.unwrap_or(u64::MAX)
});
for entry in records {
if let Some(raw) = entry.get("raw").and_then(Value::as_str) {
out.push_str(&claude_residue_line_for_emit(raw, &session_id, &cwd));
out.push('\n');
}
}
} else if let Some(extension) = native_residue_envelope(&self.meta) {
if out.is_empty() {
push_jsonl(
&mut out,
&serde_json::json!({
"type": "file-history-snapshot",
"messageId": synth_uuid(1),
"snapshot": {},
"sessionId": session_id,
"cwd": cwd,
"timestamp": SYNTH_TS,
}),
);
}
inject_first_jsonl_top_level(
&mut out,
SUPERCODE_NATIVE_RESIDUE_SUMMARY_KEY,
native_residue_summary(&extension),
);
inject_first_jsonl_top_level(&mut out, SUPERCODE_NATIVE_RESIDUE_KEY, extension);
}
out
}
#[allow(clippy::too_many_arguments)]
fn write_claude_code_records(
&self,
out: &mut String,
messages: &[ChatMessage],
session_id: &str,
cwd: &str,
mut parent: Option<String>,
mut counter: usize,
seed_used_ids: &HashSet<String>,
) {
let mut used_ids = seed_used_ids.clone();
for msg in messages {
if is_replay_excluded(msg) {
continue;
}
let blocks: Vec<Value> = match msg.role {
Role::System => {
let content = msg.content.clone().unwrap_or_default();
if content.trim().is_empty() {
continue;
}
let subtype = msg
.metadata
.get("systemSubtype")
.cloned()
.unwrap_or_else(|| "local_command".to_string());
let uuid = claude_message_uuid(msg, &mut counter, &mut used_ids);
let mut line = serde_json::json!({
"parentUuid": parent,
"type": "system",
"subtype": subtype,
"content": content,
"uuid": uuid,
"sessionId": session_id,
"cwd": cwd,
"timestamp": msg_timestamp_or_synth(msg),
});
set_grok_message_extension(&mut line, self.meta.source, msg);
push_jsonl(out, &line);
parent = Some(uuid);
continue;
}
Role::User => {
let uuid = claude_message_uuid(msg, &mut counter, &mut used_ids);
let mut line = serde_json::json!({
"parentUuid": parent,
"type": "user",
"message": {
"role": "user",
"content": claude_user_content_value(msg),
},
"uuid": uuid,
"sessionId": session_id,
"cwd": cwd,
"timestamp": msg_timestamp_or_synth(msg),
});
set_grok_message_extension(&mut line, self.meta.source, msg);
push_jsonl(out, &line);
parent = Some(uuid);
continue;
}
Role::Tool => {
let uuid = claude_message_uuid(msg, &mut counter, &mut used_ids);
let mut line = serde_json::json!({
"parentUuid": parent,
"type": "user",
"message": {
"role": "user",
"content": [{
"type": "tool_result",
"tool_use_id": msg.tool_call_id.clone().unwrap_or_default(),
"content": claude_tool_result_content_value(msg),
}],
},
"uuid": uuid,
"sessionId": session_id,
"cwd": cwd,
"timestamp": msg_timestamp_or_synth(msg),
});
if crate::tool_outcome(msg) == crate::ToolOutcome::Unknown {
line[SUPERCODE_TOOL_OUTCOME_KEY] = Value::String("unknown".to_string());
}
set_grok_message_extension(&mut line, self.meta.source, msg);
push_jsonl(out, &line);
parent = Some(uuid);
continue;
}
Role::Assistant => {
let mut blocks = Vec::new();
match msg
.metadata
.get("thinking_blocks")
.and_then(|s| serde_json::from_str::<Value>(s).ok())
.and_then(|v| v.as_array().cloned())
{
Some(saved_blocks) => blocks.extend(saved_blocks),
None => {
if let Some(t) = msg.metadata.get("thinking") {
let mut block =
serde_json::json!({"type": "thinking", "thinking": t});
if let Some(sig) = msg.metadata.get("thinking_signature") {
block["signature"] = Value::String(sig.clone());
}
blocks.push(block);
}
if let Some(rt) = msg.metadata.get("redacted_thinking") {
blocks.push(
serde_json::json!({"type": "redacted_thinking", "data": rt}),
);
}
}
}
if let Some(t) = &msg.content {
if !t.is_empty() {
blocks.push(serde_json::json!({"type": "text", "text": t}));
}
}
if let Some(parts) = &msg.content_parts {
for p in parts {
if p.get("type").and_then(Value::as_str) == Some("image_url") {
if let Some(url) = p
.get("image_url")
.and_then(|u| u.get("url"))
.and_then(Value::as_str)
{
blocks.push(match parse_data_uri(url) {
Some((mime, data)) => serde_json::json!({
"type": "image",
"source": {"type": "base64", "media_type": mime, "data": data},
}),
None => serde_json::json!({
"type": "image",
"source": {"type": "url", "url": url},
}),
});
}
}
}
}
for tc in msg.tool_calls() {
let input = tc
.function
.parsed_arguments()
.unwrap_or_else(|_| Value::Object(Default::default()));
blocks.push(serde_json::json!({
"type": "tool_use",
"id": tc.id,
"name": tc.function.name,
"input": input,
}));
}
blocks
}
};
let uuid = claude_message_uuid(msg, &mut counter, &mut used_ids);
let mut message = serde_json::json!({"role": "assistant", "content": blocks});
if let Some(model) = msg.metadata.get("model").or(self.meta.model.as_ref()) {
message["model"] = Value::String(model.clone());
}
let mut line = serde_json::json!({
"parentUuid": parent,
"type": "assistant",
"message": message,
"uuid": uuid,
"sessionId": session_id,
"cwd": cwd,
"timestamp": msg_timestamp_or_synth(msg),
});
set_grok_message_extension(&mut line, self.meta.source, msg);
push_jsonl(out, &line);
parent = Some(uuid);
}
}
pub(super) fn to_claude_code_jsonl_spliced(&self, session_id: Option<&str>) -> String {
let (raw_prefix_len, message_prefix_len) = self.spliced_prefix_lens();
let sid = session_id
.map(str::to_string)
.or_else(|| self.meta.session_id.clone())
.unwrap_or_else(|| synth_uuid(0));
let cwd = self.cwd_string();
let mut out = String::new();
let mut parent: Option<String> = None;
for line in &self.raw[..raw_prefix_len] {
push_spliced_line(&mut out, line, session_id, "sessionId");
if let Ok(v) = serde_json::from_str::<Value>(line) {
if let Some(uuid) = v.get("uuid").and_then(Value::as_str) {
parent = Some(uuid.to_string());
}
}
}
let seed_used_ids = collect_claude_uuids_from_raw(&self.raw[..raw_prefix_len]);
self.write_claude_code_records(
&mut out,
&self.messages[message_prefix_len..],
&sid,
&cwd,
parent,
1,
&seed_used_ids,
);
out
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parent_tool_use_index_matches_known_fixture_linkage() {
let main_text = r#"{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01SYjhg9qRCzUWY2GTa3iazQ","type":"tool_result","content":[{"type":"text","text":"agentId: ad8dc6cf98b49eea6"}]}]},"toolUseResult":{"agentId":"ad8dc6cf98b49eea6"}}"#;
let ids = vec![
"ad8dc6cf98b49eea6".to_string(),
"no-such-agent-id".to_string(),
];
let index = parent_tool_use_index(main_text, &ids);
assert_eq!(
index.get("ad8dc6cf98b49eea6").map(String::as_str),
Some("toolu_01SYjhg9qRCzUWY2GTa3iazQ"),
"known agent id must resolve to the pinned parent tool_use_id"
);
assert_eq!(
index.get("no-such-agent-id"),
None,
"unknown agent id must yield no entry (best-effort None)"
);
}
#[test]
fn parent_tool_use_index_empty_ids_returns_empty_map() {
let index = parent_tool_use_index("irrelevant text", &[]);
assert!(index.is_empty());
}
}