use std::collections::{BTreeMap, HashMap, HashSet};
use std::io::{BufRead, BufReader, Read, Seek, SeekFrom};
use std::path::{Path, PathBuf};
use rusqlite::Connection;
use serde_json::Value;
use crate::{
ChatMessage, Fidelity, FunctionCall, InterchangeError as Error, Result, Role, ToolCall,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SessionSource {
ClaudeCode,
Codex,
OpenCode,
Pi,
Grok,
Gemini,
Goose,
Native,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SessionFormat {
ClaudeCode,
Codex,
OpenCode,
Pi,
Grok,
Gemini,
Goose,
}
impl SessionFormat {
pub fn source(self) -> SessionSource {
match self {
SessionFormat::ClaudeCode => SessionSource::ClaudeCode,
SessionFormat::Codex => SessionSource::Codex,
SessionFormat::OpenCode => SessionSource::OpenCode,
SessionFormat::Pi => SessionSource::Pi,
SessionFormat::Grok => SessionSource::Grok,
SessionFormat::Gemini => SessionSource::Gemini,
SessionFormat::Goose => SessionSource::Goose,
}
}
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct SessionMeta {
pub source: SessionSource,
pub session_id: Option<String>,
pub model: Option<String>,
pub cwd: Option<PathBuf>,
pub system_prompt: Option<String>,
pub codex_headers: Vec<Value>,
pub codex_provenance: Vec<Value>,
pub opencode_headers: Vec<Value>,
pub goose_header: Option<Value>,
pub agent_id: Option<String>,
pub parent_tool_use_id: Option<String>,
pub lineage: std::collections::BTreeMap<String, String>,
}
impl SessionMeta {
fn new(source: SessionSource) -> Self {
SessionMeta {
source,
session_id: None,
model: None,
cwd: None,
system_prompt: None,
codex_headers: Vec::new(),
codex_provenance: Vec::new(),
opencode_headers: Vec::new(),
goose_header: None,
agent_id: None,
parent_tool_use_id: None,
lineage: std::collections::BTreeMap::new(),
}
}
}
#[derive(Debug, Clone)]
pub struct Session {
pub meta: SessionMeta,
pub messages: Vec<ChatMessage>,
pub subagents: Vec<Session>,
pub raw: Vec<String>,
pub raw_trailing_newline: bool,
pub imported_message_count: Option<usize>,
pub raw_is_verbatim: bool,
pub parse_error_lines: usize,
pub load_residue: Vec<String>,
}
impl Session {
pub fn load_fidelity(&self) -> Fidelity {
let own = if !self.load_residue.is_empty() {
Fidelity::Semantic
} else if self.raw_is_verbatim {
Fidelity::ByteLossless
} else {
Fidelity::ValueLossless
};
if own != Fidelity::Semantic
&& self
.subagents
.iter()
.any(|subagent| subagent.load_fidelity() == Fidelity::Semantic)
{
return Fidelity::Semantic;
}
own
}
pub fn from_native_messages(messages: Vec<ChatMessage>) -> Session {
Session {
meta: SessionMeta::new(SessionSource::Native),
messages,
subagents: Vec::new(),
raw: Vec::new(),
raw_trailing_newline: true,
imported_message_count: None,
raw_is_verbatim: false,
parse_error_lines: 0,
load_residue: Vec::new(),
}
}
pub fn load(path: impl AsRef<Path>) -> Result<Session> {
Self::load_with_fidelity(path, Fidelity::ByteLossless)
}
pub fn load_with_fidelity(path: impl AsRef<Path>, fidelity: Fidelity) -> Result<Session> {
Self::load_with_fidelity_and_subagents(path, fidelity, true)
}
#[doc(hidden)]
pub fn load_parent_with_fidelity(
path: impl AsRef<Path>,
fidelity: Fidelity,
) -> Result<Session> {
Self::load_with_fidelity_and_subagents(path, fidelity, false)
}
#[doc(hidden)]
pub fn load_display_view(
path: impl AsRef<Path>,
fidelity: Fidelity,
message_limit: usize,
) -> Result<Session> {
let path = path.as_ref();
if path.is_dir() || looks_like_sqlite(path) {
let mut session = Self::load_parent_with_fidelity(path, fidelity)?;
truncate_session_messages(&mut session, message_limit);
return Ok(session);
}
let mut read_limit = message_limit.max(1);
let mut previous_window_len = 0usize;
let (mut session, omitted_prefix) = loop {
let (source, text, omitted_prefix) = read_display_jsonl(path, read_limit)?;
let mut candidate = match source {
Some(SessionSource::Codex) => Self::from_codex_display_str(&text, message_limit)?,
Some(SessionSource::Gemini) => {
let mut session = Self::from_gemini_str(&text)?;
session.raw_is_verbatim = false;
session.load_residue.push(
"display history is a bounded native-record projection, not a complete Gemini artifact"
.to_string(),
);
session
}
Some(SessionSource::Pi) => Self::from_pi_str(&text)?,
Some(SessionSource::Grok) => {
let mut session = Self::from_grok_str(&text)?;
session.capture_grok_path_metadata(path);
session
}
Some(SessionSource::OpenCode) => Self::from_opencode_str(&text)?,
_ => Self::from_claude_code_str_with_fidelity(&text, fidelity)?,
};
let observed_messages = candidate
.imported_message_count
.unwrap_or(candidate.messages.len())
.max(candidate.messages.len());
let human_turns = candidate
.messages
.iter()
.filter(|message| message.role == Role::User)
.count();
let window_len = text.len();
let sufficient =
!omitted_prefix || (observed_messages > message_limit.max(1) && human_turns >= 2);
let byte_window_exhausted = window_len <= previous_window_len && read_limit >= 4096;
if sufficient || byte_window_exhausted {
if omitted_prefix {
candidate.imported_message_count =
Some(observed_messages.max(message_limit.max(1).saturating_add(1)));
}
break (candidate, omitted_prefix);
}
previous_window_len = window_len;
read_limit = read_limit.saturating_mul(2);
};
if omitted_prefix {
session.load_residue.push(
"older native records remain outside this bounded display window".to_string(),
);
}
truncate_session_messages(&mut session, message_limit);
Ok(session)
}
fn load_with_fidelity_and_subagents(
path: impl AsRef<Path>,
fidelity: Fidelity,
include_subagents: bool,
) -> Result<Session> {
let path = path.as_ref();
if path.is_dir() {
return match detect_opencode_storage_surface(path) {
Some((OpenCodeStorageSurface::Sqlite, db_path)) => {
Self::from_opencode_sqlite(&db_path, None)
}
Some((
OpenCodeStorageSurface::JsonTreeA | OpenCodeStorageSurface::JsonTreeB,
_,
)) => Err(crate::Error::Other(format!(
"{} is an OpenCode data root using a legacy JSON storage tree, which \
supercode does not load directly — point `inspect`/`convert`/`resume` \
at the store's `opencode*.db` SQLite file if this install has one, or \
use `audit --format opencode {}` instead",
path.display(),
path.display()
))),
None => Err(crate::Error::Other(format!(
"{} is a directory, but no session file or OpenCode store was found in it \
(expected an `opencode*.db` SQLite file, or an OpenCode legacy JSON storage \
tree)",
path.display()
))),
};
}
if looks_like_sqlite(path) {
return Self::from_opencode_sqlite(path, None);
}
let text = read_utf8_or_diagnose(path)?;
match detect_source(&text) {
Some(SessionSource::Codex) => Self::from_codex_str(&text),
Some(SessionSource::Pi) => Self::from_pi_str(&text),
Some(SessionSource::Grok) => {
let mut session = Self::from_grok_str(&text)?;
session.capture_grok_path_metadata(path);
Ok(session)
}
Some(SessionSource::Gemini) => Self::from_gemini_str(&text),
Some(SessionSource::Goose) => Self::from_goose_str(&text),
Some(SessionSource::OpenCode) => Self::from_opencode_str(&text),
_ => {
let mut session = Self::from_claude_code_str_with_fidelity(&text, fidelity)?;
if include_subagents {
session.attach_claude_subagents(path, &text, fidelity)?;
}
Ok(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)
}
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_codex(path: impl AsRef<Path>) -> Result<Session> {
Self::from_codex_str(&std::fs::read_to_string(path.as_ref())?)
}
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 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,
})
}
pub fn reconstruct_tree(sessions: Vec<Session>) -> Vec<Session> {
use std::collections::HashMap;
let mut idx: HashMap<String, usize> = HashMap::new();
for (i, s) in sessions.iter().enumerate() {
if let Some(id) = &s.meta.session_id {
idx.insert(id.clone(), i);
}
}
let parent_of: Vec<Option<usize>> = sessions
.iter()
.map(|s| {
s.meta
.lineage
.get("parent_thread_id")
.and_then(|p| idx.get(p).copied())
})
.collect();
let mut slots: Vec<Option<Session>> = sessions.into_iter().map(Some).collect();
let mut order: Vec<usize> = (0..slots.len()).collect();
order.sort_by_key(|&i| std::cmp::Reverse(depth_of(i, &parent_of)));
for i in order {
if let Some(p) = parent_of[i] {
if p != i {
if let Some(child) = slots[i].take() {
if let Some(parent) = slots[p].as_mut() {
parent.subagents.push(child);
} else {
slots[i] = Some(child); }
}
}
}
}
slots.into_iter().flatten().collect()
}
pub fn load_str(jsonl: &str, format: SessionFormat) -> Result<Session> {
match format {
SessionFormat::ClaudeCode => Self::from_claude_code_str(jsonl),
SessionFormat::Codex => Self::from_codex_str(jsonl),
SessionFormat::Pi => Self::from_pi_str(jsonl),
SessionFormat::OpenCode => Self::from_opencode_str(jsonl),
SessionFormat::Grok => Self::from_grok_str(jsonl),
SessionFormat::Gemini => Self::from_gemini_str(jsonl),
SessionFormat::Goose => Self::from_goose_str(jsonl),
}
}
pub fn to_jsonl(&self, format: SessionFormat) -> Result<String> {
match format {
SessionFormat::ClaudeCode => Ok(self.to_claude_code_jsonl()),
SessionFormat::Codex => Ok(self.to_codex_jsonl()),
SessionFormat::Pi => Ok(self.to_pi_jsonl()),
SessionFormat::OpenCode => self.to_opencode_jsonl(),
SessionFormat::Grok => Ok(self.to_grok_jsonl()),
SessionFormat::Gemini => Ok(self.to_gemini_jsonl()),
SessionFormat::Goose => Ok(self.to_goose_json()),
}
}
pub fn to_jsonl_spliced(
&self,
format: SessionFormat,
session_id: Option<&str>,
) -> Result<String> {
if self.parse_error_lines > 0
|| self
.subagents
.iter()
.any(|subagent| subagent.parse_error_lines > 0)
{
return Err(Error::InvalidSession(
"refusing spliced export because the loaded session contains parse loss"
.to_string(),
));
}
if self.raw.is_empty() || format.source() != self.meta.source {
if let Some(session_id) = session_id {
let mut rewritten = self.clone();
rewritten.meta.session_id = Some(session_id.to_string());
return rewritten.to_jsonl(format);
}
return self.to_jsonl(format);
}
match format {
SessionFormat::ClaudeCode => Ok(self.to_claude_code_jsonl_spliced(session_id)),
SessionFormat::Codex => Ok(self.to_codex_jsonl_spliced(session_id)),
SessionFormat::Pi => self.to_pi_jsonl_spliced(session_id),
SessionFormat::OpenCode => self.to_opencode_jsonl_spliced(session_id),
SessionFormat::Grok => Ok(self.to_grok_jsonl_spliced()),
SessionFormat::Gemini => Ok(self.to_gemini_jsonl_spliced(session_id)),
SessionFormat::Goose => Ok(self.to_goose_json_spliced(session_id)),
}
}
pub fn save(&self, path: impl AsRef<Path>, format: SessionFormat) -> Result<()> {
std::fs::write(path.as_ref(), self.to_jsonl(format)?)?;
Ok(())
}
pub fn raw_verbatim(&self) -> String {
join_lines_verbatim(&self.raw, self.raw_trailing_newline)
}
pub fn to_native_jsonl(&self) -> String {
let source = match self.meta.source {
SessionSource::ClaudeCode => "claude_code",
SessionSource::Codex => "codex",
SessionSource::Pi => "pi",
SessionSource::OpenCode => "opencode",
SessionSource::Grok => "grok",
SessionSource::Gemini => "gemini",
SessionSource::Goose => "goose",
SessionSource::Native => "native",
};
let header = serde_json::json!({
"supercode_native": 1,
"source": source,
"raw_trailing_newline": self.raw_trailing_newline,
})
.to_string();
let mut out =
String::with_capacity(self.raw.iter().map(|l| l.len() + 1).sum::<usize>() + 64);
out.push_str(&header);
out.push('\n');
for line in &self.raw {
out.push_str(line);
out.push('\n');
}
out
}
pub fn to_native_jsonl_v2(&self, appended: &[ChatMessage]) -> String {
self.to_native_jsonl_v2_with_timestamp(appended, None)
}
pub(crate) fn to_native_jsonl_v2_with_timestamp(
&self,
appended: &[ChatMessage],
fixed_timestamp: Option<&str>,
) -> String {
let source = match self.meta.source {
SessionSource::ClaudeCode => "claude_code",
SessionSource::Codex => "codex",
SessionSource::Pi => "pi",
SessionSource::OpenCode => "opencode",
SessionSource::Grok => "grok",
SessionSource::Gemini => "gemini",
SessionSource::Goose => "goose",
SessionSource::Native => "native",
};
let mut header_obj = serde_json::json!({
"supercode_native": 2,
"source": source,
"session_id": self.meta.session_id,
"created": fixed_timestamp
.map(ToOwned::to_owned)
.unwrap_or_else(crate::sidecar::now_rfc3339),
"raw_trailing_newline": self.raw_trailing_newline,
});
if let Some(obj) = header_obj.as_object_mut() {
if let Some(agent_id) = &self.meta.agent_id {
obj.insert("agent_id".to_string(), Value::String(agent_id.clone()));
}
if let Some(parent_tool_use_id) = &self.meta.parent_tool_use_id {
obj.insert(
"parent_tool_use_id".to_string(),
Value::String(parent_tool_use_id.clone()),
);
}
if !self.meta.lineage.is_empty() {
obj.insert(
"lineage".to_string(),
serde_json::to_value(&self.meta.lineage).unwrap_or(Value::Null),
);
}
}
let header = header_obj.to_string();
let mut out =
String::with_capacity(self.raw.iter().map(|l| l.len() + 1).sum::<usize>() + 64);
out.push_str(&header);
out.push('\n');
for line in &self.raw {
out.push_str(line);
out.push('\n');
}
for (turn_index, msg) in appended.iter().enumerate() {
let turn = match fixed_timestamp {
Some(timestamp) => crate::sidecar::NativeTurn::from_with_timestamp_and_index(
msg,
timestamp.to_string(),
turn_index as u64,
),
None => crate::sidecar::NativeTurn::from_with_timestamp_and_index(
msg,
crate::sidecar::now_rfc3339(),
turn_index as u64,
),
};
out.push_str(&serde_json::to_string(&turn).unwrap_or_default());
out.push('\n');
}
out
}
pub fn from_native_str(jsonl: &str) -> Result<Session> {
let (all_lines, _wrapper_trailing_newline) = split_lines_verbatim(jsonl);
let mut lines = all_lines.into_iter();
let header = lines.next().unwrap_or("");
let hv: Value = serde_json::from_str(header).unwrap_or(Value::Null);
let source = hv.get("source").and_then(Value::as_str);
let raw_trailing_newline = hv
.get("raw_trailing_newline")
.and_then(Value::as_bool)
.unwrap_or(true);
let mut body_lines: Vec<String> = Vec::new();
let mut turn_lines: Vec<&str> = Vec::new();
for line in lines {
let is_turn = serde_json::from_str::<Value>(line)
.ok()
.is_some_and(|v| v.get("supercode_turn").is_some());
if is_turn {
turn_lines.push(line);
} else {
body_lines.push(line.to_string());
}
}
let body = join_lines_verbatim(&body_lines, raw_trailing_newline);
let mut session = match source {
Some("codex") => Self::from_codex_str(&body)?,
Some("claude_code") => Self::from_claude_code_str(&body)?,
Some("pi") => Self::from_pi_str(&body)?,
Some("opencode") => Self::from_opencode_str(&body)?,
Some("grok") => Self::from_grok_str(&body)?,
Some("gemini") => Self::from_gemini_str(&body)?,
Some("goose") => Self::from_goose_str(&body)?,
Some("native") => {
let mut s = Self::from_claude_code_str(&body)?;
s.meta.source = SessionSource::Native;
s
}
_ => match detect_source(&body) {
Some(SessionSource::Codex) => Self::from_codex_str(&body)?,
Some(SessionSource::Pi) => Self::from_pi_str(&body)?,
Some(SessionSource::OpenCode) => Self::from_opencode_str(&body)?,
Some(SessionSource::Grok) => Self::from_grok_str(&body)?,
Some(SessionSource::Gemini) => Self::from_gemini_str(&body)?,
Some(SessionSource::Goose) => Self::from_goose_str(&body)?,
_ => Self::from_claude_code_str(&body)?,
},
};
for line in turn_lines {
match serde_json::from_str::<crate::sidecar::NativeTurn>(line) {
Ok(turn) => {
session.raw.push(line.to_string());
session.messages.push(turn.into_message());
}
Err(_) => {
session.raw.push(line.to_string());
session.parse_error_lines += 1;
}
}
}
if let Some(agent_id) = hv.get("agent_id").and_then(Value::as_str) {
session.meta.agent_id = Some(agent_id.to_string());
}
if let Some(parent_tool_use_id) = hv.get("parent_tool_use_id").and_then(Value::as_str) {
session.meta.parent_tool_use_id = Some(parent_tool_use_id.to_string());
}
if let Some(lineage) = hv.get("lineage").and_then(Value::as_object) {
for (k, v) in lineage {
if let Some(s) = v.as_str() {
session.meta.lineage.insert(k.clone(), s.to_string());
}
}
}
Ok(session)
}
pub fn from_sidecar_str(s: &str) -> Result<Session> {
let header = s.lines().next().ok_or_else(|| {
Error::InvalidSession("sidecar header is missing from an empty artifact".to_string())
})?;
let value: Value = serde_json::from_str(header).map_err(|error| {
Error::InvalidSession(format!("sidecar header is not valid JSON: {error}"))
})?;
let version = value.get("supercode_native").and_then(Value::as_u64);
if !matches!(version, Some(1 | 2)) {
return Err(Error::InvalidSession(
"sidecar header must declare supported `supercode_native` version 1 or 2"
.to_string(),
));
}
let source = value.get("source").and_then(Value::as_str);
if !matches!(
source,
Some(
"native"
| "claude_code"
| "codex"
| "gemini"
| "goose"
| "opencode"
| "pi"
| "grok"
)
) {
return Err(Error::InvalidSession(
"sidecar header must declare a supported `source`".to_string(),
));
}
Self::from_native_str(s)
}
pub fn from_codex_str(jsonl: &str) -> Result<Session> {
let mut meta = SessionMeta::new(SessionSource::Codex);
let mut messages = Vec::new();
let assistant_texts = collect_codex_assistant_texts(jsonl);
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 pending_reasoning = String::new();
let mut pending_reasoning_content = String::new();
let mut pending_reasoning_encrypted = false;
let mut parse_error_lines = 0usize;
let mut restored_embedded_codex_provenance = false;
for (record_index, raw_line) in raw_lines.iter().enumerate() {
let line = raw_line.trim();
if line.is_empty() {
continue;
}
let v: Value = match serde_json::from_str(line) {
Ok(v) => v,
Err(_) => {
parse_error_lines += 1;
continue;
}
};
let payload = v.get("payload").unwrap_or(&Value::Null);
if !restored_embedded_codex_provenance
&& v.get("type").and_then(Value::as_str) == Some("session_meta")
&& payload
.get(SUPERCODE_CODEX_PROVENANCE_KEY)
.map(|extension| restore_codex_provenance(extension, &mut meta))
.transpose()?
.unwrap_or(false)
{
restored_embedded_codex_provenance = true;
}
if !restored_embedded_codex_provenance {
capture_codex_provenance_record(&mut meta, record_index, raw_line, &v);
}
let line_ts = v.get("timestamp").and_then(Value::as_str);
match v.get("type").and_then(Value::as_str) {
Some("session_meta") => {
capture_codex_session_meta(payload, &mut meta);
if !restored_embedded_codex_provenance {
meta.codex_headers.push(v.clone());
}
}
Some("turn_context") => {
if meta.model.is_none() {
meta.model = payload
.get("model")
.and_then(Value::as_str)
.map(str::to_string);
}
if !restored_embedded_codex_provenance {
meta.codex_headers.push(v.clone());
}
}
Some("response_item")
if payload.get("type").and_then(Value::as_str) == Some("reasoning") =>
{
let summary = extract_text_content(payload.get("summary"));
if !summary.trim().is_empty() {
push_str_field(&mut pending_reasoning, &summary);
}
if let Some(raw_content) = payload.get("content").filter(|v| !v.is_null()) {
let text = extract_text_content(Some(raw_content));
if !text.trim().is_empty() {
push_str_field(&mut pending_reasoning_content, &text);
}
}
if payload
.get("encrypted_content")
.is_some_and(|v| !v.is_null())
{
pending_reasoning_encrypted = true;
}
}
Some("response_item") => {
let before = messages.len();
push_codex_item(payload, &mut messages);
if messages.len() > before
&& (!pending_reasoning.is_empty()
|| !pending_reasoning_content.is_empty()
|| pending_reasoning_encrypted)
{
let is_assistant = messages
.last()
.map(|m| m.role == Role::Assistant)
.unwrap_or(false);
if is_assistant {
let last = messages.last_mut().expect("checked above");
if !pending_reasoning.is_empty() {
last.metadata.insert(
"reasoning".to_string(),
std::mem::take(&mut pending_reasoning),
);
}
if !pending_reasoning_content.is_empty() {
last.metadata.insert(
"reasoning_content".to_string(),
std::mem::take(&mut pending_reasoning_content),
);
}
if pending_reasoning_encrypted {
last.metadata
.insert("reasoning_encrypted".to_string(), "true".to_string());
pending_reasoning_encrypted = false;
}
} else {
let orphan = orphaned_reasoning_message(
&mut pending_reasoning,
&mut pending_reasoning_content,
&mut pending_reasoning_encrypted,
);
messages.insert(before, orphan);
}
}
stamp_new_codex_messages(&mut messages, before, line_ts);
restore_single_grok_message(payload, &mut messages[before..]);
}
Some("compacted") => {
messages.clear();
if let Some(Value::Array(history)) = payload.get("replacement_history") {
for item in history {
push_codex_item(item, &mut messages);
}
}
stamp_new_codex_messages(&mut messages, 0, line_ts);
if let Some(last) = messages.last_mut() {
last.metadata.remove("__codex_open_turn");
}
}
Some("event_msg")
if payload.get("type").and_then(Value::as_str) == Some("agent_message") =>
{
let before = messages.len();
let text = agent_message_text(payload);
if !text.trim().is_empty() && !assistant_texts.contains(text.trim()) {
push_assistant(&mut messages, text, Vec::new());
if let Some(last) = messages.last_mut() {
if let Some(phase) = payload.get("phase").and_then(Value::as_str) {
last.metadata.insert("phase".to_string(), phase.to_string());
}
}
}
stamp_new_codex_messages(&mut messages, before, line_ts);
}
Some("event_msg")
if payload.get("type").and_then(Value::as_str)
== Some("thread_rolled_back") =>
{
let n = payload
.get("num_turns")
.and_then(Value::as_u64)
.unwrap_or(1);
for _ in 0..n {
remove_last_turn(&mut messages);
}
}
Some("event_msg")
if payload.get("type").and_then(Value::as_str)
== Some("thread_goal_updated") =>
{
let before = messages.len();
let goal = payload.get("goal");
if let Some(obj) = goal
.and_then(|g| g.get("objective"))
.and_then(Value::as_str)
{
if !obj.trim().is_empty() {
messages.push(ChatMessage::system(format!("[thread goal] {obj}")));
if let Some(last) = messages.last_mut() {
if let Some(status) =
goal.and_then(|g| g.get("status")).and_then(Value::as_str)
{
last.metadata
.insert("goal_status".to_string(), status.to_string());
}
if let Some(budget) = goal
.and_then(|g| g.get("tokenBudget"))
.and_then(Value::as_i64)
{
last.metadata.insert(
"goal_token_budget".to_string(),
budget.to_string(),
);
}
}
}
}
stamp_new_codex_messages(&mut messages, before, line_ts);
}
Some("event_msg")
if payload.get("type").and_then(Value::as_str)
== Some("exited_review_mode") =>
{
let before = messages.len();
if let Some(review) = payload.get("review_output") {
let text = review
.get("overall_explanation")
.and_then(Value::as_str)
.map(str::to_string)
.unwrap_or_else(|| review.to_string());
push_assistant(&mut messages, format!("[code review] {text}"), Vec::new());
if let Some(findings) = review.get("findings") {
if findings.as_array().is_some_and(|a| !a.is_empty()) {
if let Some(last) = messages.last_mut() {
if let Ok(s) = serde_json::to_string(findings) {
last.metadata.insert("review_findings".to_string(), s);
}
}
}
}
if let Some(last) = messages.last_mut() {
if let Some(correctness) =
review.get("overall_correctness").and_then(Value::as_str)
{
last.metadata.insert(
"review_overall_correctness".to_string(),
correctness.to_string(),
);
}
if let Some(score) = review
.get("overall_confidence_score")
.and_then(Value::as_f64)
{
last.metadata.insert(
"review_overall_confidence_score".to_string(),
score.to_string(),
);
}
}
}
stamp_new_codex_messages(&mut messages, before, line_ts);
}
_ => {} }
}
if !pending_reasoning.is_empty()
|| !pending_reasoning_content.is_empty()
|| pending_reasoning_encrypted
{
let orphan = orphaned_reasoning_message(
&mut pending_reasoning,
&mut pending_reasoning_content,
&mut pending_reasoning_encrypted,
);
messages.push(orphan);
}
ensure_tool_results_paired(&mut messages);
for m in &mut messages {
m.metadata.remove("__codex_open_turn");
if m.metadata
.remove("__grok_remove_synthetic_turn_id")
.is_some()
{
m.metadata.remove("turn_id");
}
}
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: Vec::new(),
})
}
fn from_codex_display_str(jsonl: &str, message_limit: usize) -> Result<Session> {
let mut meta = SessionMeta::new(SessionSource::Codex);
let mut messages: Vec<ChatMessage> = Vec::new();
let mut preceding_users = Vec::new();
let mut parse_error_lines = 0usize;
let mut record_count = 0usize;
let mut total_message_count = 0usize;
let retain = message_limit.max(1).saturating_add(64);
let mut canonical_assistant_texts = HashSet::new();
for raw_line in non_empty_lines(jsonl) {
record_count += 1;
let value: Value = match serde_json::from_str(raw_line) {
Ok(value) => value,
Err(_) => {
parse_error_lines += 1;
continue;
}
};
let payload = value.get("payload").unwrap_or(&Value::Null);
let line_ts = value.get("timestamp").and_then(Value::as_str);
match value.get("type").and_then(Value::as_str) {
Some("session_meta") => capture_codex_session_meta(payload, &mut meta),
Some("turn_context") if meta.model.is_none() => {
meta.model = payload
.get("model")
.and_then(Value::as_str)
.map(str::to_string);
}
Some("response_item")
if payload.get("type").and_then(Value::as_str) != Some("reasoning") =>
{
let assistant_text = (payload.get("type").and_then(Value::as_str)
== Some("message")
&& payload.get("role").and_then(Value::as_str) == Some("assistant"))
.then(|| extract_text_content(payload.get("content")))
.filter(|text| !text.trim().is_empty());
if let Some(text) = assistant_text.as_deref() {
if let Some(index) = messages.iter().rposition(|message| {
message.metadata.contains_key("codex_event_message")
&& message.content.as_deref() == Some(text)
}) {
messages.remove(index);
total_message_count = total_message_count.saturating_sub(1);
}
canonical_assistant_texts.insert(text.trim().to_string());
}
let before = messages.len();
push_codex_item(payload, &mut messages);
total_message_count += messages.len().saturating_sub(before);
stamp_new_codex_messages(&mut messages, before, line_ts);
restore_single_grok_message(payload, &mut messages[before..]);
}
Some("event_msg")
if payload.get("type").and_then(Value::as_str) == Some("agent_message") =>
{
let text = agent_message_text(payload);
if !text.trim().is_empty() && !canonical_assistant_texts.contains(text.trim()) {
let before = messages.len();
push_assistant(&mut messages, text, Vec::new());
total_message_count += 1;
if let Some(last) = messages.last_mut() {
last.metadata
.insert("codex_event_message".to_string(), "true".to_string());
if let Some(phase) = payload.get("phase").and_then(Value::as_str) {
last.metadata.insert("phase".to_string(), phase.to_string());
}
}
stamp_new_codex_messages(&mut messages, before, line_ts);
}
}
_ => {}
}
if messages.len() > retain {
let remove = messages.len() - retain;
for message in messages.drain(..remove) {
if message.role == Role::User {
preceding_users.push(message);
if preceding_users.len() > 2 {
preceding_users.remove(0);
}
}
}
}
}
for message in &mut messages {
message.metadata.remove("__codex_open_turn");
message.metadata.remove("codex_event_message");
if message
.metadata
.remove("__grok_remove_synthetic_turn_id")
.is_some()
{
message.metadata.remove("turn_id");
}
}
truncate_messages_with_anchor(&mut messages, message_limit, preceding_users);
let imported_message_count = Some(total_message_count);
Ok(Session {
meta,
messages,
subagents: Vec::new(),
raw: vec![String::new(); record_count],
raw_trailing_newline: jsonl.ends_with('\n'),
imported_message_count,
raw_is_verbatim: false,
parse_error_lines,
load_residue: vec![
"display history is a bounded native-record projection, not resumable model context"
.to_string(),
],
})
}
pub fn from_pi(path: impl AsRef<Path>) -> Result<Session> {
Self::from_pi_str(&std::fs::read_to_string(path.as_ref())?)
}
pub fn from_pi_str(jsonl: &str) -> Result<Session> {
let mut meta = SessionMeta::new(SessionSource::Pi);
let (raw_lines, raw_trailing_newline) = split_lines_verbatim(jsonl);
let raw: Vec<String> = raw_lines.iter().map(|l| l.to_string()).collect();
let non_empty_line_count = non_empty_lines(jsonl).count();
let lines_v: Vec<Value> = non_empty_lines(jsonl)
.filter_map(|l| serde_json::from_str(l).ok())
.collect();
let parse_error_lines = non_empty_line_count.saturating_sub(lines_v.len());
if let Some(header) = lines_v.first() {
capture_pi_header(header, &mut meta)?;
}
struct PiEntry {
id: String,
parent_id: Option<String>,
value: Value,
}
let mut entries: Vec<PiEntry> = Vec::new();
let mut by_id: HashMap<String, usize> = HashMap::new();
for v in lines_v.iter().skip(1) {
let Some(id) = v.get("id").and_then(Value::as_str) else {
continue;
};
let parent_id = v
.get("parentId")
.and_then(Value::as_str)
.map(str::to_string);
by_id.insert(id.to_string(), entries.len());
entries.push(PiEntry {
id: id.to_string(),
parent_id,
value: v.clone(),
});
}
if entries.is_empty() {
return Ok(Session {
meta,
messages: Vec::new(),
subagents: Vec::new(),
raw,
raw_trailing_newline,
imported_message_count: Some(0),
raw_is_verbatim: true,
parse_error_lines,
load_residue: Vec::new(),
});
}
let leaf_idx = entries.len() - 1;
let mut chain_rev: Vec<usize> = Vec::new();
let mut cur: Option<String> = Some(entries[leaf_idx].id.clone());
let mut guard = 0usize;
while let Some(id) = cur {
let Some(&idx) = by_id.get(&id) else { break };
chain_rev.push(idx);
cur = entries[idx].parent_id.clone();
guard += 1;
if guard > entries.len() + 1 {
break; }
}
chain_rev.reverse();
let active = chain_rev;
let pos_in_active: HashMap<&str, usize> = active
.iter()
.enumerate()
.map(|(pos, &idx)| (entries[idx].id.as_str(), pos))
.collect();
let mut kept_from_pos = 0usize;
for &idx in &active {
let e = &entries[idx];
if e.value.get("type").and_then(Value::as_str) == Some("compaction") {
if let Some(fk) = e.value.get("firstKeptEntryId").and_then(Value::as_str) {
if let Some(&p) = pos_in_active.get(fk) {
kept_from_pos = kept_from_pos.max(p);
}
}
}
}
let mut messages = Vec::new();
let mut current_model: Option<String> = None;
for (pos, &idx) in active.iter().enumerate() {
let e = &entries[idx];
let v = &e.value;
let entry_ts = v
.get("timestamp")
.and_then(Value::as_str)
.map(str::to_string);
let before = messages.len();
match v.get("type").and_then(Value::as_str) {
Some("message") => {
let msg_v = v.get("message").cloned().unwrap_or(Value::Null);
match msg_v.get("role").and_then(Value::as_str) {
Some("user") => push_pi_user(&msg_v, &mut messages),
Some("assistant") => {
push_pi_assistant(&msg_v, &mut messages);
if let Some(m) = msg_v.get("model").and_then(Value::as_str) {
current_model = Some(m.to_string());
}
}
Some("toolResult") => push_pi_tool_result(&msg_v, &mut messages),
Some("bashExecution") => push_pi_bash(&msg_v, &mut messages),
Some("custom") => push_pi_custom_common(&msg_v, &mut messages),
_ => {}
}
}
Some("custom_message") => push_pi_custom_common(v, &mut messages),
Some("compaction") => push_pi_compaction(v, &mut messages),
Some("branch_summary") => push_pi_branch_summary(v, &mut messages),
Some("model_change") => {
if let Some(m) = v.get("modelId").and_then(Value::as_str) {
current_model = Some(m.to_string());
}
}
Some("session_info") => {
if let Some(name) = v.get("name").and_then(Value::as_str) {
if !name.is_empty() {
meta.lineage
.insert("session_name".to_string(), name.to_string());
}
}
}
_ => {}
}
let is_summary = matches!(
v.get("type").and_then(Value::as_str),
Some("compaction") | Some("branch_summary")
);
for m in &mut messages[before..] {
m.metadata.insert("pi_entry_id".to_string(), e.id.clone());
if let Some(p) = &e.parent_id {
m.metadata.insert("pi_parent_id".to_string(), p.clone());
}
if let Some(ts) = &entry_ts {
m.metadata
.entry("timestamp".to_string())
.or_insert_with(|| ts.clone());
}
if let Some(ms) = v
.get("message")
.and_then(|mm| mm.get("timestamp"))
.and_then(Value::as_u64)
{
m.metadata
.entry("timestamp".to_string())
.or_insert_with(|| crate::sidecar::ms_to_rfc3339(ms as i64));
}
if !is_summary && pos < kept_from_pos {
m.metadata
.insert("compacted_out".to_string(), "true".to_string());
}
}
restore_single_grok_message(v, &mut messages[before..]);
for message in &mut messages[before..] {
restore_tool_outcome_extension(v, message);
}
}
meta.model = current_model;
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: Vec::new(),
})
}
pub fn from_grok(path: impl AsRef<Path>) -> Result<Session> {
let path = path.as_ref();
let mut session = Self::from_grok_str(&std::fs::read_to_string(path)?)?;
session.capture_grok_path_metadata(path);
Ok(session)
}
pub fn from_grok_str(jsonl: &str) -> Result<Session> {
let mut meta = SessionMeta::new(SessionSource::Grok);
let (raw_lines, raw_trailing_newline) = split_lines_verbatim(jsonl);
let raw: Vec<String> = raw_lines.iter().map(|line| line.to_string()).collect();
let mut messages = Vec::new();
let mut parse_error_lines = 0usize;
let mut tool_names: HashMap<String, String> = HashMap::new();
for line in non_empty_lines(jsonl) {
let value: Value = match serde_json::from_str(line) {
Ok(value) => value,
Err(_) => {
parse_error_lines += 1;
continue;
}
};
restore_codex_provenance_from_top_level(&value, &mut meta)?;
match value.get("type").and_then(Value::as_str) {
Some("system") => {
if meta.system_prompt.is_none() {
meta.system_prompt = value
.get("content")
.and_then(Value::as_str)
.map(str::to_string);
}
}
Some("user") => {
let content = extract_text_content(value.get("content"));
let role = if value.get("synthetic_reason").and_then(Value::as_str)
== Some("supercode_system_event")
{
Role::System
} else {
Role::User
};
let content = if role == Role::User {
match grok_human_user_text(&content) {
Some(content) => content,
None if value.get(SUPERCODE_GROK_MESSAGE_KEY).is_some() => {
String::new()
}
None => continue,
}
} else {
content
};
let mut message = ChatMessage {
role,
content: Some(content),
content_parts: None,
tool_calls: None,
tool_call_id: None,
name: None,
metadata: Default::default(),
};
capture_grok_scalar_metadata(
&value,
&mut message,
&["prompt_index", "prior_turn_interrupt", "synthetic_reason"],
);
restore_grok_message_extension(&value, &mut message);
messages.push(message);
}
Some("assistant") => {
let calls: Vec<ToolCall> = value
.get("tool_calls")
.and_then(Value::as_array)
.into_iter()
.flatten()
.filter_map(|call| {
let id = call.get("id")?.as_str()?.to_string();
let name = call.get("name")?.as_str()?.to_string();
let arguments = call
.get("arguments")
.map(value_to_arg_string)
.unwrap_or_else(|| "{}".to_string());
tool_names.insert(id.clone(), name.clone());
Some(function_call(&id, &name, arguments))
})
.collect();
let content = value
.get("content")
.and_then(Value::as_str)
.filter(|content| !content.is_empty())
.map(str::to_string);
let mut message = ChatMessage {
role: Role::Assistant,
content,
content_parts: None,
tool_calls: (!calls.is_empty()).then_some(calls),
tool_call_id: None,
name: None,
metadata: Default::default(),
};
capture_grok_scalar_metadata(
&value,
&mut message,
&["model_id", "model_fingerprint", "reasoning_effort"],
);
if let Some(model) = value.get("model_id").and_then(Value::as_str) {
meta.model = Some(model.to_string());
}
restore_grok_message_extension(&value, &mut message);
messages.push(message);
}
Some("tool_result") => {
let id = value
.get("tool_call_id")
.and_then(Value::as_str)
.unwrap_or_default();
let content = value
.get("content")
.map(|value| match value {
Value::String(text) => text.clone(),
other => extract_text_content(Some(other)),
})
.unwrap_or_default();
let mut message = tool_message(id, content);
message.name = tool_names.get(id).cloned();
restore_grok_message_extension(&value, &mut message);
messages.push(message);
}
_ => {}
}
}
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: Vec::new(),
})
}
fn capture_grok_path_metadata(&mut self, transcript: &Path) {
let Some(session_dir) = transcript.parent() else {
return;
};
self.meta.session_id = session_dir
.file_name()
.and_then(|name| name.to_str())
.map(str::to_string);
self.meta.cwd = session_dir
.parent()
.and_then(Path::file_name)
.and_then(|name| name.to_str())
.and_then(percent_decode_path)
.map(PathBuf::from);
let Ok(summary_text) = std::fs::read_to_string(session_dir.join("summary.json")) else {
return;
};
let Ok(summary) = serde_json::from_str::<Value>(&summary_text) else {
return;
};
if let Some(model) = summary.get("current_model_id").and_then(Value::as_str) {
self.meta.model = Some(model.to_string());
}
for (source, target) in [
("generated_title", "session_name"),
("created_at", "created_at"),
("updated_at", "updated_at"),
("chat_format_version", "grok_chat_format_version"),
] {
if let Some(value) = summary.get(source) {
self.meta.lineage.insert(
target.to_string(),
value
.as_str()
.map(str::to_string)
.unwrap_or_else(|| value.to_string()),
);
}
}
}
pub fn from_gemini(path: impl AsRef<Path>) -> Result<Session> {
Self::from_gemini_str(&std::fs::read_to_string(path.as_ref())?)
}
pub fn from_gemini_str(jsonl: &str) -> Result<Session> {
let mut meta = SessionMeta::new(SessionSource::Gemini);
let (raw_lines, raw_trailing_newline) = split_lines_verbatim(jsonl);
let raw = raw_lines.iter().map(|line| line.to_string()).collect();
let mut messages = Vec::new();
let mut parse_error_lines = 0usize;
let mut pending_by_name: HashMap<String, Vec<String>> = HashMap::new();
for (line_index, line) in non_empty_lines(jsonl).enumerate() {
let value: Value = match serde_json::from_str(line) {
Ok(value) => value,
Err(_) => {
parse_error_lines += 1;
continue;
}
};
let kind = value.get("type").and_then(Value::as_str);
if kind.is_none() {
if meta.session_id.is_none() {
meta.session_id = value
.get("sessionId")
.and_then(Value::as_str)
.map(str::to_string);
}
for (source, target) in [
("projectHash", "gemini_project_hash"),
("startTime", "created_at"),
("lastUpdated", "updated_at"),
("kind", "gemini_session_kind"),
] {
if let Some(raw) = value.get(source) {
meta.lineage.insert(
target.to_string(),
raw.as_str()
.map(str::to_string)
.unwrap_or_else(|| raw.to_string()),
);
}
}
continue;
}
if kind != Some("user") && kind != Some("gemini") {
continue;
}
let timestamp = value.get("timestamp").and_then(Value::as_str);
let model = value.get("model").and_then(Value::as_str);
if let Some(model) = model {
meta.model = Some(model.to_string());
}
let content = value.get("content").unwrap_or(&Value::Null);
let parts = content.as_array();
let text = match content {
Value::String(text) => text.clone(),
Value::Array(parts) => parts
.iter()
.filter_map(|part| part.get("text").and_then(Value::as_str))
.collect::<Vec<_>>()
.join(" ")
.trim()
.to_string(),
_ => String::new(),
};
if kind == Some("gemini") {
let legacy_calls = parts
.into_iter()
.flatten()
.filter_map(|part| part.get("functionCall"));
let native_calls = value
.get("toolCalls")
.and_then(Value::as_array)
.into_iter()
.flatten();
let calls = native_calls
.chain(legacy_calls)
.enumerate()
.filter_map(|(call_index, call)| {
let name = call.get("name")?.as_str()?.to_string();
let id = call
.get("id")
.and_then(Value::as_str)
.map(str::to_string)
.unwrap_or_else(|| format!("gemini-{line_index}-{call_index}"));
pending_by_name
.entry(name.clone())
.or_default()
.push(id.clone());
let arguments = call
.get("args")
.map(value_to_arg_string)
.unwrap_or_else(|| "{}".to_string());
Some(function_call(&id, &name, arguments))
})
.collect::<Vec<_>>();
let mut message = ChatMessage {
role: Role::Assistant,
content: (!text.is_empty()).then_some(text),
content_parts: None,
tool_calls: (!calls.is_empty()).then_some(calls),
tool_call_id: None,
name: None,
metadata: Default::default(),
};
if let Some(timestamp) = timestamp {
message
.metadata
.insert("timestamp".into(), timestamp.into());
}
if let Some(model) = model {
message.metadata.insert("gemini_model".into(), model.into());
}
if let Some(thoughts) = value.get("thoughts").filter(|value| !value.is_null()) {
message
.metadata
.insert("gemini_thoughts".into(), thoughts.to_string());
}
restore_gemini_message_extension(&value, &mut message);
if message.content.is_some() || message.tool_calls.is_some() {
messages.push(message);
}
continue;
}
let mut user_parts = Vec::new();
if let Some(parts) = parts {
for part in parts {
if let Some(response) = part.get("functionResponse") {
push_gemini_user_parts(
&mut messages,
std::mem::take(&mut user_parts),
timestamp,
&value,
);
let name = response
.get("name")
.and_then(Value::as_str)
.unwrap_or("tool")
.to_string();
let explicit_id = response
.get("id")
.and_then(Value::as_str)
.map(str::to_string);
if let Some(id) = explicit_id.as_deref() {
if let Some(ids) = pending_by_name.get_mut(&name) {
if let Some(position) = ids.iter().position(|pending| pending == id)
{
ids.remove(position);
}
}
}
let id = explicit_id
.or_else(|| {
pending_by_name
.get_mut(&name)
.and_then(|ids| (!ids.is_empty()).then(|| ids.remove(0)))
})
.unwrap_or_else(|| format!("gemini-{line_index}-response"));
let output = response
.get("response")
.and_then(|response| response.get("output"))
.map(|output| {
output
.as_str()
.map(str::to_string)
.unwrap_or_else(|| output.to_string())
})
.or_else(|| response.get("response").map(Value::to_string))
.unwrap_or_default();
let mut message = tool_message(&id, output);
message.name = Some(name);
if let Some(timestamp) = timestamp {
message
.metadata
.insert("timestamp".into(), timestamp.into());
}
restore_gemini_message_extension(&value, &mut message);
messages.push(message);
continue;
}
if let Some(text) = part.get("text").and_then(Value::as_str) {
user_parts.push(serde_json::json!({"type": "text", "text": text}));
continue;
}
if let Some(inline) = part.get("inlineData") {
let Some(data) = inline.get("data").and_then(Value::as_str) else {
continue;
};
let media_type = inline
.get("mimeType")
.and_then(Value::as_str)
.unwrap_or("application/octet-stream");
user_parts.push(serde_json::json!({
"type": "image_url",
"image_url": {"url": format!("data:{media_type};base64,{data}")},
}));
}
}
} else if !text.is_empty() {
user_parts.push(serde_json::json!({"type": "text", "text": text}));
}
push_gemini_user_parts(&mut messages, user_parts, timestamp, &value);
}
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: Vec::new(),
})
}
pub fn from_goose(path: impl AsRef<Path>) -> Result<Session> {
Self::from_goose_str(&std::fs::read_to_string(path.as_ref())?)
}
pub fn from_goose_str(json: &str) -> Result<Session> {
let document: Value = serde_json::from_str(json).map_err(crate::Error::Decode)?;
let object = document.as_object().ok_or_else(|| {
Error::InvalidSession("Goose session export must be a JSON object".to_string())
})?;
let conversation = object
.get("conversation")
.and_then(Value::as_array)
.ok_or_else(|| {
Error::InvalidSession(
"Goose session export must contain a conversation array".to_string(),
)
})?;
let mut meta = SessionMeta::new(SessionSource::Goose);
meta.session_id = object.get("id").and_then(Value::as_str).map(str::to_string);
meta.cwd = object
.get("working_dir")
.or_else(|| object.get("workingDir"))
.and_then(Value::as_str)
.map(PathBuf::from);
meta.model = object
.get("model_config")
.or_else(|| object.get("modelConfig"))
.and_then(|model| model.get("model_name").or_else(|| model.get("modelName")))
.and_then(Value::as_str)
.map(str::to_string);
for (source, target) in [
("name", "session_name"),
("created_at", "created_at"),
("updated_at", "updated_at"),
("session_type", "goose_session_type"),
("goose_mode", "goose_mode"),
("provider_name", "goose_provider_name"),
("parent_session_id", "parent_session_id"),
] {
if let Some(value) = object.get(source) {
meta.lineage.insert(
target.to_string(),
value
.as_str()
.map(str::to_string)
.unwrap_or_else(|| value.to_string()),
);
}
}
let mut header = document.clone();
if let Some(header) = header.as_object_mut() {
header.remove("conversation");
}
meta.goose_header = Some(header.clone());
let mut messages = Vec::new();
for (native_index, native) in conversation.iter().enumerate() {
let before = messages.len();
normalize_goose_message(native, native_index, &mut messages);
if let Some(first) = messages.get_mut(before) {
first
.metadata
.insert("goose_native_message".to_string(), native.to_string());
first
.metadata
.insert("goose_native_index".to_string(), native_index.to_string());
if native_index == 0 {
first
.metadata
.insert("goose_session_header".to_string(), header.to_string());
}
restore_grok_message_extension(native, first);
}
for message in messages.iter_mut().skip(before + 1) {
message
.metadata
.insert("goose_native_index".to_string(), native_index.to_string());
}
}
ensure_tool_results_paired(&mut messages);
let (raw_lines, raw_trailing_newline) = split_lines_verbatim(json);
let raw = raw_lines.iter().map(|line| line.to_string()).collect();
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: 0,
load_residue: Vec::new(),
})
}
pub fn from_goose_sqlite(db_path: &Path, session_id: &str) -> Result<Session> {
Self::from_goose_sqlite_with_limit(db_path, session_id, None)
}
#[doc(hidden)]
pub fn from_goose_sqlite_display(
db_path: &Path,
session_id: &str,
message_limit: usize,
) -> Result<Session> {
Self::from_goose_sqlite_with_limit(db_path, session_id, Some(message_limit.max(1)))
}
fn from_goose_sqlite_with_limit(
db_path: &Path,
session_id: &str,
message_limit: Option<usize>,
) -> Result<Session> {
let connection = Connection::open_with_flags(
db_path,
rusqlite::OpenFlags::SQLITE_OPEN_READ_ONLY | rusqlite::OpenFlags::SQLITE_OPEN_NO_MUTEX,
)
.map_err(|error| Error::Other(format!("failed to open Goose SQLite store: {error}")))?;
let mut statement = connection
.prepare(
"SELECT id, name, working_dir, created_at, updated_at, session_type, \
extension_data, goose_mode, provider_name, model_config_json \
FROM sessions WHERE id = ?1",
)
.map_err(|error| Error::Other(format!("failed to query Goose sessions: {error}")))?;
let mut document = statement
.query_row([session_id], |row| {
let extension_data: Option<String> = row.get(6)?;
let model_config: Option<String> = row.get(9)?;
Ok(serde_json::json!({
"id": row.get::<_, String>(0)?,
"working_dir": row.get::<_, String>(2)?,
"name": row.get::<_, String>(1)?,
"user_set_name": false,
"session_type": row.get::<_, String>(5)?,
"created_at": row.get::<_, String>(3)?,
"updated_at": row.get::<_, String>(4)?,
"extension_data": extension_data
.as_deref()
.and_then(|value| serde_json::from_str::<Value>(value).ok())
.unwrap_or_else(|| serde_json::json!({})),
"usage": {},
"accumulated_usage": {},
"accumulated_cost": Value::Null,
"schedule_id": Value::Null,
"recipe": Value::Null,
"user_recipe_values": Value::Null,
"conversation": [],
"message_count": 0,
"last_message_at": Value::Null,
"provider_name": row.get::<_, Option<String>>(8)?,
"model_config": model_config
.as_deref()
.and_then(|value| serde_json::from_str::<Value>(value).ok()),
"goose_mode": row.get::<_, String>(7)?,
"archived_at": Value::Null,
"project_id": Value::Null,
"parent_session_id": Value::Null,
"last_message_snippet": Value::Null,
}))
})
.map_err(|error| Error::Other(format!("failed to load Goose session: {error}")))?;
let message_query = message_limit.map_or_else(
|| {
"SELECT message_id, role, content_json, created_timestamp, metadata_json \
FROM messages WHERE session_id = ?1 ORDER BY created_timestamp, id"
.to_string()
},
|limit| {
format!(
"SELECT message_id, role, content_json, created_timestamp, metadata_json \
FROM (SELECT id AS native_row_id, message_id, role, content_json, \
created_timestamp, metadata_json \
FROM messages WHERE session_id = ?1 \
ORDER BY created_timestamp DESC, id DESC LIMIT {limit}) \
ORDER BY created_timestamp, native_row_id"
)
},
);
let mut message_statement = connection
.prepare(&message_query)
.map_err(|error| Error::Other(format!("failed to query Goose messages: {error}")))?;
let rows = message_statement
.query_map([session_id], |row| {
let content: String = row.get(2)?;
let metadata: Option<String> = row.get(4)?;
Ok(serde_json::json!({
"id": row.get::<_, Option<String>>(0)?,
"role": row.get::<_, String>(1)?,
"created": row.get::<_, i64>(3)?,
"content": serde_json::from_str::<Value>(&content)
.unwrap_or_else(|_| Value::Array(Vec::new())),
"metadata": metadata
.as_deref()
.and_then(|value| serde_json::from_str::<Value>(value).ok())
.unwrap_or_else(|| serde_json::json!({
"userVisible": true,
"agentVisible": true
})),
}))
})
.map_err(|error| Error::Other(format!("failed to load Goose messages: {error}")))?;
let conversation = rows
.collect::<std::result::Result<Vec<_>, _>>()
.map_err(|error| Error::Other(format!("failed to decode Goose messages: {error}")))?;
document["message_count"] = Value::from(conversation.len());
document["conversation"] = Value::Array(conversation);
let json = serde_json::to_string_pretty(&document).map_err(crate::Error::Decode)?;
let mut session = Self::from_goose_str(&json)?;
session.raw_is_verbatim = false;
Ok(session)
}
pub fn from_opencode(path: impl AsRef<Path>) -> Result<Session> {
Self::from_opencode_str(&std::fs::read_to_string(path.as_ref())?)
}
pub fn from_opencode_sqlite(db_path: &Path, session_id: Option<&str>) -> Result<Session> {
let conn = opencode_sqlite_open(db_path)?;
let id = match session_id {
Some(id) => id.to_string(),
None => opencode_sqlite_primary_session_id(&conn)?,
};
let lines = opencode_sqlite_session_envelope_lines(&conn, db_path, &id)?;
let mut text = lines.join("\n");
text.push('\n');
let mut session = Self::from_opencode_str(&text)?;
session.raw_is_verbatim = false;
Ok(session)
}
pub fn from_opencode_str(text: &str) -> Result<Session> {
let trimmed = text.trim();
if let Ok(doc) = serde_json::from_str::<Value>(trimmed) {
if doc.get("info").is_some() && doc.get("messages").and_then(Value::as_array).is_some()
{
return Self::from_opencode_export_doc(&doc);
}
}
let (raw_lines, raw_trailing_newline) = split_lines_verbatim(text);
let raw: Vec<String> = raw_lines.iter().map(|l| l.to_string()).collect();
let mut session_info: Option<Value> = None;
let mut side_records: Vec<Value> = Vec::new();
let mut msgs: Vec<OcMsg> = Vec::new();
let mut msg_index: HashMap<String, usize> = HashMap::new();
let mut parse_error_lines = 0usize;
for line in non_empty_lines(text) {
let Ok(env) = serde_json::from_str::<Value>(line) else {
parse_error_lines += 1;
continue; };
let Some(key) = env.get("key").and_then(Value::as_array) else {
continue; };
let value = env.get("value").cloned().unwrap_or(Value::Null);
match key.first().and_then(Value::as_str) {
Some("session") => session_info = Some(value),
Some("message") => {
let Some(id) = value.get("id").and_then(Value::as_str) else {
continue;
};
let time_created = value
.get("time")
.and_then(|t| t.get("created"))
.and_then(Value::as_i64)
.unwrap_or(0);
msg_index.insert(id.to_string(), msgs.len());
msgs.push(OcMsg {
id: id.to_string(),
time_created,
value,
parts: Vec::new(),
});
}
Some("part") => {
if let Some(msg_id) = value.get("messageID").and_then(Value::as_str) {
if let Some(&idx) = msg_index.get(msg_id) {
msgs[idx].parts.push(value);
}
}
}
Some("session_diff") | Some("todo") => {
side_records.push(serde_json::json!({"key": key, "value": value}));
}
_ => {} }
}
opencode_guard_against_silent_empty(
!trimmed.is_empty(),
&session_info,
&msgs,
&side_records,
)?;
opencode_session_from_records(
session_info,
side_records,
msgs,
raw,
raw_trailing_newline,
true,
parse_error_lines,
)
}
fn from_opencode_export_doc(doc: &Value) -> Result<Session> {
let session_info = doc.get("info").cloned().filter(|v| !v.is_null());
let messages_arr = doc
.get("messages")
.and_then(Value::as_array)
.cloned()
.unwrap_or_default();
let session_id = session_info
.as_ref()
.and_then(|si| si.get("id"))
.and_then(Value::as_str)
.unwrap_or("ses_unknown")
.to_string();
let project_id = session_info
.as_ref()
.and_then(|si| si.get("projectID"))
.and_then(Value::as_str)
.unwrap_or("global")
.to_string();
let mut raw: Vec<String> = Vec::new();
if let Some(si) = &session_info {
raw.push(
serde_json::json!({"key": ["session", project_id, session_id], "value": si})
.to_string(),
);
}
let mut msgs: Vec<OcMsg> = Vec::new();
for entry in &messages_arr {
let Some(info) = entry.get("info") else {
continue; };
let Some(id) = info.get("id").and_then(Value::as_str) else {
continue;
};
let time_created = info
.get("time")
.and_then(|t| t.get("created"))
.and_then(Value::as_i64)
.unwrap_or(0);
let parts: Vec<Value> = entry
.get("parts")
.and_then(Value::as_array)
.cloned()
.unwrap_or_default();
raw.push(
serde_json::json!({"key": ["message", session_id, id], "value": info}).to_string(),
);
for p in &parts {
let part_id = p.get("id").and_then(Value::as_str).unwrap_or("");
raw.push(serde_json::json!({"key": ["part", id, part_id], "value": p}).to_string());
}
msgs.push(OcMsg {
id: id.to_string(),
time_created,
value: info.clone(),
parts,
});
}
opencode_guard_against_silent_empty(true, &session_info, &msgs, &[])?;
opencode_session_from_records(
session_info,
Vec::new(),
msgs,
raw,
true,
false,
0,
)
}
pub fn to_session_tree(&self, created_at_ms: i64) -> crate::session_tree::SessionTree {
crate::session_tree::SessionTree::from_linear(&self.messages, created_at_ms)
}
pub fn apply_session_tree(&mut self, tree: &crate::session_tree::SessionTree) -> Result<()> {
self.messages = tree.linear_projection()?;
Ok(())
}
}
struct OcMsg {
id: String,
time_created: i64,
value: Value,
parts: Vec<Value>,
}
const OPENCODE_SUPERCODE_MESSAGE_POSITION: &str = "_supercode_message_position";
const OPENCODE_SUPERCODE_RESULT_POSITION: &str = "_supercode_result_position";
const OPENCODE_INTERNAL_ORIGINAL_POSITION: &str = "__supercode_original_position";
fn opencode_guard_against_silent_empty(
non_empty_input: bool,
session_info: &Option<Value>,
msgs: &[OcMsg],
side_records: &[Value],
) -> Result<()> {
let has_any_record = session_info.as_ref().is_some_and(|v| !v.is_null())
|| !msgs.is_empty()
|| !side_records.is_empty();
if non_empty_input && !has_any_record {
return Err(crate::Error::Other(
"opencode input was recognized as an OpenCode source (envelope or \
export-document form) but no session/message/part record could be parsed from \
it — refusing to silently return an empty session"
.to_string(),
));
}
Ok(())
}
fn opencode_session_from_records(
session_info: Option<Value>,
side_records: Vec<Value>,
mut msgs: Vec<OcMsg>,
raw: Vec<String>,
raw_trailing_newline: bool,
raw_is_verbatim: bool,
parse_error_lines: usize,
) -> Result<Session> {
let mut meta = SessionMeta::new(SessionSource::OpenCode);
let msg_index: HashMap<String, usize> = msgs
.iter()
.enumerate()
.map(|(i, m)| (m.id.clone(), i))
.collect();
msgs.sort_by(|a, b| {
a.time_created
.cmp(&b.time_created)
.then_with(|| a.id.cmp(&b.id))
});
for m in &mut msgs {
m.parts.sort_by(|a, b| {
let ai = a.get("id").and_then(Value::as_str).unwrap_or("");
let bi = b.get("id").and_then(Value::as_str).unwrap_or("");
ai.cmp(bi)
});
}
meta.opencode_headers
.push(session_info.clone().unwrap_or(Value::Null));
meta.opencode_headers.extend(side_records);
if let Some(si) = &session_info {
capture_opencode_session_info(si, &mut meta)?;
}
let mut tail_start_pos: Option<usize> = None;
for m in &msgs {
for p in &m.parts {
if p.get("type").and_then(Value::as_str) == Some("compaction") {
if let Some(t) = p.get("tail_start_id").and_then(Value::as_str) {
if let Some(&tp) = msg_index.get(t) {
tail_start_pos = Some(tail_start_pos.map_or(tp, |cur| cur.max(tp)));
}
}
}
}
}
let mut messages = Vec::new();
let mut first_system_seen = false;
for (pos, m) in msgs.iter().enumerate() {
let before = messages.len();
match m.value.get("role").and_then(Value::as_str) {
Some("user") => match opencode_claude_system_subtype(&m.parts) {
Some(subtype) => {
push_opencode_claude_system(&m.value, &m.parts, subtype, &mut messages)
}
None => push_opencode_user(
&m.value,
&m.parts,
&mut messages,
&mut meta,
&mut first_system_seen,
),
},
Some("assistant") => {
push_opencode_assistant(&m.value, &m.parts, &mut messages, &mut meta)
}
_ => {}
}
if let Some(original_position) = m
.value
.get(OPENCODE_SUPERCODE_MESSAGE_POSITION)
.and_then(Value::as_u64)
{
if let Some(message) = messages[before..]
.iter_mut()
.find(|message| message.role != Role::Tool)
{
message.metadata.insert(
OPENCODE_INTERNAL_ORIGINAL_POSITION.to_string(),
original_position.to_string(),
);
}
}
for msg in &mut messages[before..] {
let is_summary = msg.metadata.get("is_summary").map(String::as_str) == Some("true");
if !is_summary {
if let Some(tsp) = tail_start_pos {
if pos < tsp {
msg.metadata
.insert("compacted_out".to_string(), "true".to_string());
}
}
}
}
}
let marked_slots = messages
.iter()
.enumerate()
.filter_map(|(index, message)| {
message
.metadata
.contains_key(OPENCODE_INTERNAL_ORIGINAL_POSITION)
.then_some(index)
})
.collect::<Vec<_>>();
if !marked_slots.is_empty() {
let mut marked_messages = marked_slots
.iter()
.map(|index| messages[*index].clone())
.collect::<Vec<_>>();
marked_messages.sort_by_key(|message| {
message
.metadata
.get(OPENCODE_INTERNAL_ORIGINAL_POSITION)
.and_then(|position| position.parse::<usize>().ok())
.unwrap_or(usize::MAX)
});
for (slot, message) in marked_slots.into_iter().zip(marked_messages) {
messages[slot] = message;
}
for message in &mut messages {
message.metadata.remove(OPENCODE_INTERNAL_ORIGINAL_POSITION);
}
}
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,
parse_error_lines,
load_residue: Vec::new(),
})
}
pub fn resolve_opencode_parent_tool_use_ids(sessions: &mut [Session]) {
let ids: Vec<Option<String>> = sessions.iter().map(|s| s.meta.session_id.clone()).collect();
for i in 0..sessions.len() {
let child_id = sessions[i].meta.session_id.clone();
let parent_id = sessions[i].meta.lineage.get("parent_session_id").cloned();
let (Some(child_id), Some(parent_id)) = (child_id, parent_id) else {
continue;
};
let Some(parent_idx) = ids
.iter()
.position(|id| id.as_deref() == Some(parent_id.as_str()))
else {
continue;
};
for m in &sessions[parent_idx].messages {
for (k, v) in &m.metadata {
if let Some(call_id) = k.strip_prefix("oc_task_child_session_id__") {
if v == &child_id {
sessions[i].meta.parent_tool_use_id = Some(call_id.to_string());
}
}
}
}
}
}
fn reorder_tool_results_after_calls(messages: &mut Vec<ChatMessage>) {
let mut owner_positions: HashMap<String, usize> = HashMap::new();
for (index, m) in messages.iter().enumerate() {
if m.role == Role::Assistant {
for c in m.tool_calls() {
if !c.id.is_empty() {
owner_positions.entry(c.id.clone()).or_insert(index);
}
}
}
}
let mut contiguous_owner = None;
let needs_reorder =
messages
.iter()
.enumerate()
.any(|(message_index, message)| match message.role {
Role::Assistant => {
contiguous_owner = Some(message_index);
false
}
Role::Tool => match message
.tool_call_id
.as_deref()
.and_then(|id| owner_positions.get(id))
.copied()
{
Some(owner) => Some(owner) != contiguous_owner,
None => {
contiguous_owner = None;
false
}
},
_ => {
contiguous_owner = None;
false
}
});
if !needs_reorder {
return;
}
let mut spine: Vec<ChatMessage> = Vec::with_capacity(messages.len());
let mut call_owner: HashMap<String, usize> = HashMap::new();
let mut owned_results: Vec<(usize, ChatMessage)> = Vec::new();
let drained: Vec<ChatMessage> = std::mem::take(messages);
for (orig_pos, msg) in drained.into_iter().enumerate() {
if msg.role == Role::Tool {
let is_owned = msg
.tool_call_id
.as_deref()
.map(|id| !id.is_empty() && owner_positions.contains_key(id))
.unwrap_or(false);
if is_owned {
owned_results.push((orig_pos, msg));
continue;
}
spine.push(msg);
continue;
}
if msg.role == Role::Assistant {
let spine_idx = spine.len();
for c in msg.tool_calls() {
if !c.id.is_empty() {
call_owner.entry(c.id.clone()).or_insert(spine_idx);
}
}
}
spine.push(msg);
}
let mut buckets: HashMap<usize, Vec<(usize, ChatMessage)>> = HashMap::new();
for (orig_pos, msg) in owned_results.into_iter() {
let id = msg
.tool_call_id
.as_deref()
.filter(|id| !id.is_empty())
.expect("routed as owned, so tool_call_id must be a non-empty owned id");
let idx = *call_owner
.get(id)
.expect("owned id must have an owning assistant in call_owner");
buckets.entry(idx).or_default().push((orig_pos, msg));
}
for v in buckets.values_mut() {
v.sort_by_key(|(pos, _)| *pos);
}
let mut out: Vec<ChatMessage> = Vec::with_capacity(spine.len() + buckets.len());
for (idx, msg) in spine.into_iter().enumerate() {
out.push(msg);
if let Some(results) = buckets.remove(&idx) {
for (_, r) in results {
out.push(r);
}
}
}
*messages = out;
}
fn ensure_tool_results_paired(messages: &mut Vec<ChatMessage>) {
let answered: HashSet<String> = messages
.iter()
.filter(|m| m.role == Role::Tool)
.filter_map(|m| m.tool_call_id.clone())
.collect();
let any_missing = messages.iter().any(|m| {
m.tool_calls()
.iter()
.any(|c| !c.id.is_empty() && !answered.contains(&c.id))
});
if !any_missing {
return;
}
let mut out: Vec<ChatMessage> = Vec::with_capacity(messages.len() + 4);
for msg in messages.drain(..) {
let synth: Vec<ChatMessage> = msg
.tool_calls()
.iter()
.filter(|c| !c.id.is_empty() && !answered.contains(&c.id))
.map(|c| {
let mut m = ChatMessage::tool_result(
c.id.clone(),
c.function.name.clone(),
"[no tool result recorded — turn interrupted]".to_string(),
);
crate::mark_tool_error(&mut m);
m
})
.collect();
out.push(msg);
out.extend(synth);
}
*messages = out;
}
fn is_replay_excluded(msg: &ChatMessage) -> bool {
msg.metadata.get("compacted_out").map(String::as_str) == Some("true")
|| msg
.metadata
.get("pi_exclude_from_context")
.map(String::as_str)
== Some("true")
}
fn detect_source(text: &str) -> Option<SessionSource> {
if let Ok(v) = serde_json::from_str::<Value>(text.trim()) {
if v.get("conversation").and_then(Value::as_array).is_some()
&& (v.get("working_dir").is_some() || v.get("workingDir").is_some())
{
return Some(SessionSource::Goose);
}
if v.get("info").is_some() && v.get("messages").and_then(Value::as_array).is_some() {
return Some(SessionSource::OpenCode);
}
}
for line in non_empty_lines(text) {
let Ok(v) = serde_json::from_str::<Value>(line) else {
continue;
};
if v.get("key").and_then(Value::as_array).is_some() && v.get("value").is_some() {
return Some(SessionSource::OpenCode);
}
if v.get("payload").is_some() {
return Some(SessionSource::Codex);
}
if v.get("sessionId").and_then(Value::as_str).is_some()
&& (v.get("projectHash").is_some()
|| v.get("startTime").is_some()
|| v.get("lastUpdated").is_some())
&& v.get("type").is_none()
{
return Some(SessionSource::Gemini);
}
let tag = v.get("type").and_then(Value::as_str);
if tag == Some("gemini") && v.get("content").is_some() {
return Some(SessionSource::Gemini);
}
if v.get("message").is_none()
&& v.get("uuid").is_none()
&& v.get("sessionId").is_none()
&& matches!(
tag,
Some(
"system"
| "user"
| "assistant"
| "tool_result"
| "reasoning"
| "backend_tool_call"
)
)
&& (v.get("content").is_some()
|| v.get("tool_calls").is_some()
|| v.get("tool_call_id").is_some()
|| v.get("encrypted_content").is_some()
|| v.get("kind").is_some())
{
return Some(SessionSource::Grok);
}
if v.get("type").and_then(Value::as_str) == Some("session")
&& v.get("id").and_then(Value::as_str).is_some()
&& v.get("message").is_none()
&& v.get("uuid").is_none()
{
return Some(SessionSource::Pi);
}
if v.get("type").is_some() || v.get("message").is_some() {
return Some(SessionSource::ClaudeCode);
}
}
None
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OpenCodeStorageSurface {
Sqlite,
JsonTreeB,
JsonTreeA,
}
pub fn detect_opencode_storage_surface(
data_root: &Path,
) -> Option<(OpenCodeStorageSurface, PathBuf)> {
if let Ok(p) = std::env::var("OPENCODE_DB") {
let pb = PathBuf::from(p);
if pb.is_file() {
return Some((OpenCodeStorageSurface::Sqlite, pb));
}
}
if let Ok(entries) = std::fs::read_dir(data_root) {
let mut candidates: Vec<PathBuf> = entries
.flatten()
.map(|entry| entry.path())
.filter(|p| {
p.file_name()
.and_then(|n| n.to_str())
.is_some_and(|name| name.starts_with("opencode") && name.ends_with(".db"))
})
.collect();
candidates.sort();
if let Some(exact) = candidates
.iter()
.find(|p| p.file_name().and_then(|n| n.to_str()) == Some("opencode.db"))
{
return Some((OpenCodeStorageSurface::Sqlite, exact.clone()));
}
if let Some(first) = candidates.into_iter().next() {
return Some((OpenCodeStorageSurface::Sqlite, first));
}
}
let storage = data_root.join("storage");
if storage.join("migration").is_file() {
return Some((OpenCodeStorageSurface::JsonTreeB, storage));
}
let project_dir = data_root.join("project");
if project_dir.is_dir() {
return Some((OpenCodeStorageSurface::JsonTreeA, project_dir));
}
None
}
fn split_lines_verbatim(text: &str) -> (Vec<&str>, bool) {
if text.is_empty() {
return (Vec::new(), false);
}
let ends_with_newline = text.ends_with('\n');
let body = if ends_with_newline {
&text[..text.len() - 1]
} else {
text
};
(body.split('\n').collect(), ends_with_newline)
}
fn join_lines_verbatim(lines: &[String], ends_with_newline: bool) -> String {
let mut out = lines.join("\n");
if ends_with_newline {
out.push('\n');
}
out
}
const SQLITE_MAGIC: &[u8; 16] = b"SQLite format 3\0";
pub fn looks_like_sqlite(path: &Path) -> bool {
if !path.is_file() {
return false;
}
if path.extension().and_then(|e| e.to_str()) == Some("db") {
return true;
}
use std::io::Read;
let Ok(mut f) = std::fs::File::open(path) else {
return false;
};
let mut buf = [0u8; 16];
f.read_exact(&mut buf).is_ok() && &buf == SQLITE_MAGIC
}
fn read_utf8_or_diagnose(path: &Path) -> Result<String> {
let bytes = std::fs::read(path)?;
String::from_utf8(bytes).map_err(|_| {
crate::Error::Other(format!(
"{} is not valid UTF-8 text, and is not a recognized OpenCode SQLite store \
(no `SQLite format 3` header) — supercode reads Claude Code / Codex / Pi / \
OpenCode session logs as UTF-8 JSONL, or an OpenCode `opencode*.db` SQLite file",
path.display()
))
})
}
fn read_display_jsonl(
path: &Path,
message_limit: usize,
) -> Result<(Option<SessionSource>, String, bool)> {
const MIN_TAIL_BYTES: u64 = 4 * 1024 * 1024;
const MAX_TAIL_BYTES: u64 = 64 * 1024 * 1024;
const BYTES_PER_MESSAGE: u64 = 16 * 1024;
let mut first = String::new();
BufReader::new(std::fs::File::open(path)?).read_line(&mut first)?;
let source = detect_source(&first);
if !matches!(
source,
Some(SessionSource::ClaudeCode | SessionSource::Codex | SessionSource::Gemini)
) {
let text = read_utf8_or_diagnose(path)?;
return Ok((detect_source(&text), text, false));
}
let mut file = std::fs::File::open(path)?;
let file_len = file.metadata()?.len();
let requested = (message_limit.max(1) as u64)
.saturating_mul(BYTES_PER_MESSAGE)
.clamp(MIN_TAIL_BYTES, MAX_TAIL_BYTES);
if file_len <= requested {
let text = read_utf8_or_diagnose(path)?;
return Ok((source, text, false));
}
let start = file_len - requested;
file.seek(SeekFrom::Start(start))?;
let mut bytes = Vec::with_capacity(requested as usize);
file.read_to_end(&mut bytes)?;
if let Some(newline) = bytes.iter().position(|byte| *byte == b'\n') {
bytes.drain(..=newline);
}
let mut tail = String::from_utf8(bytes).map_err(|_| {
crate::Error::Other(format!(
"{} contains non-UTF-8 data in its display window",
path.display()
))
})?;
if !tail
.lines()
.any(|line| native_display_human_line(line, source))
{
let search_bytes = file_len.min(requested.saturating_mul(2).min(MAX_TAIL_BYTES));
let search_start = file_len - search_bytes;
file.seek(SeekFrom::Start(search_start))?;
let mut search = Vec::with_capacity(search_bytes as usize);
file.read_to_end(&mut search)?;
if search_start > 0 {
if let Some(newline) = search.iter().position(|byte| *byte == b'\n') {
search.drain(..=newline);
}
}
if let Ok(search) = std::str::from_utf8(&search) {
if let Some(anchor) = search
.lines()
.rev()
.find(|line| native_display_human_line(line, source))
{
tail = format!("{anchor}\n{tail}");
}
}
}
let text = if matches!(source, Some(SessionSource::Codex | SessionSource::Gemini)) {
format!("{first}{tail}")
} else {
tail
};
Ok((source, text, true))
}
fn native_display_human_line(line: &str, source: Option<SessionSource>) -> bool {
if !line
.as_bytes()
.windows(6)
.any(|window| window == b"\"user\"")
{
return false;
}
let Ok(value) = serde_json::from_str::<Value>(line) else {
return false;
};
match source {
Some(SessionSource::Codex) => {
value.get("type").and_then(Value::as_str) == Some("response_item")
&& value
.get("payload")
.and_then(|payload| payload.get("type"))
.and_then(Value::as_str)
== Some("message")
&& value
.get("payload")
.and_then(|payload| payload.get("role"))
.and_then(Value::as_str)
== Some("user")
}
Some(SessionSource::ClaudeCode) => {
value.get("type").and_then(Value::as_str) == Some("user")
&& value
.get("message")
.and_then(|message| message.get("content"))
.is_some_and(|content| match content {
Value::String(text) => !text.trim().is_empty(),
Value::Array(parts) => parts.iter().any(|part| {
part.get("type").and_then(Value::as_str) == Some("text")
&& part
.get("text")
.and_then(Value::as_str)
.is_some_and(|text| !text.trim().is_empty())
}),
_ => false,
})
}
Some(SessionSource::Gemini) => {
value.get("type").and_then(Value::as_str) == Some("user")
&& value.get("content").is_some_and(|content| match content {
Value::String(text) => !text.trim().is_empty(),
Value::Array(parts) => parts.iter().any(|part| {
part.get("text")
.and_then(Value::as_str)
.is_some_and(|text| !text.trim().is_empty())
}),
_ => false,
})
}
_ => false,
}
}
fn opencode_sql_err(e: rusqlite::Error, context: &str) -> crate::Error {
crate::Error::Other(format!("OpenCode SQLite error while {context}: {e}"))
}
fn opencode_sqlite_open(db_path: &Path) -> Result<Connection> {
if !db_path.is_file() {
return Err(crate::Error::Other(format!(
"OpenCode SQLite store not found at {} — expected an `opencode*.db` file \
(see `docs/interop/opencode-pi-spec.md` §1.2)",
db_path.display()
)));
}
let conn = Connection::open_with_flags(
db_path,
rusqlite::OpenFlags::SQLITE_OPEN_READ_ONLY | rusqlite::OpenFlags::SQLITE_OPEN_NO_MUTEX,
)
.map_err(|e| {
crate::Error::Other(format!(
"{} does not look like a valid OpenCode SQLite database: {e}",
db_path.display()
))
})?;
let has_session_table: i64 = conn
.query_row(
"SELECT count(*) FROM sqlite_master WHERE type='table' AND name='session'",
[],
|r| r.get(0),
)
.map_err(|e| {
crate::Error::Other(format!(
"failed to read the OpenCode SQLite schema at {}: {e}",
db_path.display()
))
})?;
if has_session_table == 0 {
return Err(crate::Error::Other(format!(
"{} is a SQLite database but has no `session` table — not a recognized \
OpenCode store (wrong file, or an unsupported/pre-SQLite OpenCode version)",
db_path.display()
)));
}
Ok(conn)
}
fn opencode_json_col(s: Option<String>, col: &str, context: &str) -> Value {
match s.as_deref() {
None => Value::Null,
Some(t) => match serde_json::from_str::<Value>(t) {
Ok(v) => v,
Err(e) => {
tracing::warn!(
column = col,
context,
error = %e,
"opencode SQLite column failed to parse as JSON — treating as absent (D7)"
);
Value::Null
}
},
}
}
fn opencode_session_columns(
conn: &Connection,
) -> rusqlite::Result<std::collections::HashSet<String>> {
let mut stmt = conn.prepare("PRAGMA table_info(session)")?;
let names = stmt.query_map([], |r| r.get::<_, String>(1))?; names.collect()
}
fn opencode_row_session_info(conn: &Connection, session_id: &str) -> Result<Value> {
let cols = opencode_session_columns(conn)
.map_err(|e| opencode_sql_err(e, &format!("reading session `{session_id}` schema")))?;
let has = |name: &str| cols.contains(name);
conn.query_row("SELECT * FROM session WHERE id = ?1", [session_id], |r| {
let id: String = r.get("id")?;
let project_id: String = r.get("project_id")?;
let workspace_id: Option<String> = if has("workspace_id") {
r.get("workspace_id")?
} else {
None
};
let parent_id: Option<String> = r.get("parent_id")?;
let slug: String = r.get("slug")?;
let directory: String = r.get("directory")?;
let path: Option<String> = if has("path") { r.get("path")? } else { None };
let title: String = r.get("title")?;
let version: String = r.get("version")?;
let share_url: Option<String> = r.get("share_url")?;
let summary_additions: Option<i64> = r.get("summary_additions")?;
let summary_deletions: Option<i64> = r.get("summary_deletions")?;
let summary_files: Option<i64> = r.get("summary_files")?;
let summary_diffs: Option<String> = r.get("summary_diffs")?;
let metadata: Option<String> = if has("metadata") {
r.get("metadata")?
} else {
None
};
let cost: f64 = if has("cost") { r.get("cost")? } else { 0.0 };
let tokens_input: i64 = if has("tokens_input") {
r.get("tokens_input")?
} else {
0
};
let tokens_output: i64 = if has("tokens_output") {
r.get("tokens_output")?
} else {
0
};
let tokens_reasoning: i64 = if has("tokens_reasoning") {
r.get("tokens_reasoning")?
} else {
0
};
let tokens_cache_read: i64 = if has("tokens_cache_read") {
r.get("tokens_cache_read")?
} else {
0
};
let tokens_cache_write: i64 = if has("tokens_cache_write") {
r.get("tokens_cache_write")?
} else {
0
};
let revert: Option<String> = r.get("revert")?;
let permission: Option<String> = if has("permission") {
r.get("permission")?
} else {
None
};
let agent: Option<String> = if has("agent") { r.get("agent")? } else { None };
let model: Option<String> = if has("model") { r.get("model")? } else { None };
let time_created: i64 = r.get("time_created")?;
let time_updated: i64 = r.get("time_updated")?;
let time_compacting: Option<i64> = if has("time_compacting") {
r.get("time_compacting")?
} else {
None
};
let time_archived: Option<i64> = if has("time_archived") {
r.get("time_archived")?
} else {
None
};
let summary =
(summary_additions.is_some() || summary_deletions.is_some() || summary_files.is_some())
.then(|| {
serde_json::json!({
"additions": summary_additions.unwrap_or(0),
"deletions": summary_deletions.unwrap_or(0),
"files": summary_files.unwrap_or(0),
"diffs": opencode_json_col(summary_diffs, "summary_diffs", session_id),
})
});
let share = share_url.map(|u| serde_json::json!({"url": u}));
Ok(serde_json::json!({
"id": id,
"slug": slug,
"projectID": project_id,
"workspaceID": workspace_id,
"directory": directory,
"path": path,
"parentID": parent_id,
"summary": summary,
"cost": cost,
"tokens": {
"input": tokens_input,
"output": tokens_output,
"reasoning": tokens_reasoning,
"cache": {"read": tokens_cache_read, "write": tokens_cache_write},
},
"share": share,
"title": title,
"agent": agent,
"model": opencode_json_col(model, "model", session_id),
"version": version,
"metadata": opencode_json_col(metadata, "metadata", session_id),
"time": {
"created": time_created,
"updated": time_updated,
"compacting": time_compacting,
"archived": time_archived,
},
"permission": opencode_json_col(permission, "permission", session_id),
"revert": opencode_json_col(revert, "revert", session_id),
}))
})
.map_err(|e| match e {
rusqlite::Error::QueryReturnedNoRows => crate::Error::Other(format!(
"OpenCode session `{session_id}` not found in this SQLite store"
)),
e => opencode_sql_err(e, &format!("reading session `{session_id}`")),
})
}
fn opencode_row_message_value(
id: &str,
session_id: &str,
data_json: &str,
time_created: i64,
time_updated: i64,
) -> Value {
let mut v = opencode_json_col(Some(data_json.to_string()), "message.data", id);
if let Value::Object(map) = &mut v {
map.insert("id".to_string(), Value::String(id.to_string()));
map.insert(
"sessionID".to_string(),
Value::String(session_id.to_string()),
);
map.insert("time_created".to_string(), Value::from(time_created));
map.insert("time_updated".to_string(), Value::from(time_updated));
}
v
}
fn opencode_row_part_value(
id: &str,
session_id: &str,
message_id: &str,
data_json: &str,
time_created: i64,
time_updated: i64,
) -> Value {
let mut v = opencode_json_col(Some(data_json.to_string()), "part.data", id);
if let Value::Object(map) = &mut v {
map.insert("id".to_string(), Value::String(id.to_string()));
map.insert(
"sessionID".to_string(),
Value::String(session_id.to_string()),
);
map.insert(
"messageID".to_string(),
Value::String(message_id.to_string()),
);
map.insert("time_created".to_string(), Value::from(time_created));
map.insert("time_updated".to_string(), Value::from(time_updated));
}
v
}
fn opencode_sqlite_session_envelope_lines(
conn: &Connection,
db_path: &Path,
session_id: &str,
) -> Result<Vec<String>> {
let mut lines = Vec::new();
let session_info = opencode_row_session_info(conn, session_id)?;
let project_id = session_info
.get("projectID")
.and_then(Value::as_str)
.unwrap_or("global")
.to_string();
lines.push(
serde_json::json!({"key": ["session", project_id, session_id], "value": session_info})
.to_string(),
);
let mut msg_stmt = conn
.prepare(
"SELECT id, data, time_created, time_updated FROM message \
WHERE session_id = ?1 ORDER BY time_created, id",
)
.map_err(|e| opencode_sql_err(e, "preparing the message query"))?;
let msg_rows = msg_stmt
.query_map([session_id], |r| {
let id: String = r.get("id")?;
let data: String = r.get("data")?;
let time_created: i64 = r.get("time_created")?;
let time_updated: i64 = r.get("time_updated")?;
Ok((id, data, time_created, time_updated))
})
.map_err(|e| opencode_sql_err(e, "querying messages"))?;
let mut part_stmt = conn
.prepare(
"SELECT id, data, time_created, time_updated FROM part \
WHERE message_id = ?1 ORDER BY id",
)
.map_err(|e| opencode_sql_err(e, "preparing the part query"))?;
for row in msg_rows {
let (msg_id, data, msg_time_created, msg_time_updated) =
row.map_err(|e| opencode_sql_err(e, "reading a message row"))?;
let msg_value = opencode_row_message_value(
&msg_id,
session_id,
&data,
msg_time_created,
msg_time_updated,
);
lines.push(
serde_json::json!({"key": ["message", session_id, msg_id], "value": msg_value})
.to_string(),
);
let part_rows = part_stmt
.query_map([&msg_id], |r| {
let id: String = r.get("id")?;
let data: String = r.get("data")?;
let time_created: i64 = r.get("time_created")?;
let time_updated: i64 = r.get("time_updated")?;
Ok((id, data, time_created, time_updated))
})
.map_err(|e| opencode_sql_err(e, "querying parts"))?;
for prow in part_rows {
let (part_id, pdata, part_time_created, part_time_updated) =
prow.map_err(|e| opencode_sql_err(e, "reading a part row"))?;
let part_value = opencode_row_part_value(
&part_id,
session_id,
&msg_id,
&pdata,
part_time_created,
part_time_updated,
);
lines.push(
serde_json::json!({"key": ["part", msg_id, part_id], "value": part_value})
.to_string(),
);
}
}
let mut todo_stmt = conn
.prepare(
"SELECT content, status, priority, position, time_created, time_updated \
FROM todo WHERE session_id = ?1 ORDER BY position",
)
.map_err(|e| opencode_sql_err(e, "preparing the todo query"))?;
let todo_rows = todo_stmt
.query_map([session_id], |r| {
let content: String = r.get("content")?;
let status: String = r.get("status")?;
let priority: String = r.get("priority")?;
let position: i64 = r.get("position")?;
let time_created: i64 = r.get("time_created")?;
let time_updated: i64 = r.get("time_updated")?;
Ok(serde_json::json!({
"sessionID": session_id,
"content": content,
"status": status,
"priority": priority,
"position": position,
"time": {"created": time_created, "updated": time_updated},
}))
})
.map_err(|e| opencode_sql_err(e, "querying todos"))?;
for trow in todo_rows {
let tv = trow.map_err(|e| opencode_sql_err(e, "reading a todo row"))?;
let position = tv.get("position").cloned().unwrap_or(Value::Null);
lines.push(
serde_json::json!({"key": ["todo", session_id, position], "value": tv}).to_string(),
);
}
if let Some(diff_value) = opencode_read_session_diff_sidecar(db_path, session_id) {
lines.push(
serde_json::json!({"key": ["session_diff", session_id], "value": diff_value})
.to_string(),
);
}
Ok(lines)
}
fn opencode_read_session_diff_sidecar(db_path: &Path, session_id: &str) -> Option<Value> {
let dir = db_path.parent()?;
let sidecar = dir
.join("storage")
.join("session_diff")
.join(format!("{session_id}.json"));
let text = std::fs::read_to_string(&sidecar).ok()?;
match serde_json::from_str::<Value>(&text) {
Ok(v) => Some(v),
Err(e) => {
tracing::warn!(
path = %sidecar.display(),
error = %e,
"opencode session_diff sidecar failed to parse as JSON — skipping (D7)"
);
None
}
}
}
fn opencode_sqlite_primary_session_id(conn: &Connection) -> Result<String> {
conn.query_row(
"SELECT id FROM session ORDER BY (parent_id IS NULL) DESC, time_updated DESC LIMIT 1",
[],
|r| r.get::<_, String>(0),
)
.map_err(|e| match e {
rusqlite::Error::QueryReturnedNoRows => {
crate::Error::Other("OpenCode SQLite store contains no sessions".to_string())
}
e => opencode_sql_err(e, "selecting the primary session"),
})
}
fn opencode_sqlite_all_session_ids(conn: &Connection, limit: Option<usize>) -> Result<Vec<String>> {
let mut stmt = conn
.prepare("SELECT id FROM session ORDER BY time_created, id")
.map_err(|e| opencode_sql_err(e, "listing sessions"))?;
let rows = stmt
.query_map([], |r| r.get::<_, String>(0))
.map_err(|e| opencode_sql_err(e, "listing sessions"))?;
let mut ids = Vec::new();
for row in rows {
ids.push(row.map_err(|e| opencode_sql_err(e, "reading a session id"))?);
if limit.is_some_and(|n| ids.len() >= n) {
break;
}
}
Ok(ids)
}
pub fn opencode_sqlite_session_ids(db_path: &Path) -> Result<Vec<String>> {
let conn = opencode_sqlite_open(db_path)?;
opencode_sqlite_all_session_ids(&conn, None)
}
pub fn opencode_sqlite_primary_id(db_path: &Path) -> Result<String> {
let conn = opencode_sqlite_open(db_path)?;
opencode_sqlite_primary_session_id(&conn)
}
#[derive(Debug, Clone, Copy, Default)]
#[non_exhaustive]
pub struct OpenCodeSqliteStoreStats {
pub sessions: u64,
pub messages: u64,
pub parts: u64,
pub todos: u64,
}
pub fn opencode_sqlite_store_stats(db_path: &Path) -> Result<OpenCodeSqliteStoreStats> {
let conn = opencode_sqlite_open(db_path)?;
let count = |table: &str| -> Result<u64> {
let sql = format!("SELECT count(*) FROM {table}");
conn.query_row(&sql, [], |r| r.get::<_, i64>(0))
.map(|n| n.max(0) as u64)
.map_err(|e| opencode_sql_err(e, &format!("counting `{table}` rows")))
};
Ok(OpenCodeSqliteStoreStats {
sessions: count("session")?,
messages: count("message")?,
parts: count("part")?,
todos: count("todo")?,
})
}
pub fn opencode_sqlite_corpus_envelope_text(
db_path: &Path,
limit_sessions: Option<usize>,
) -> Result<String> {
let conn = opencode_sqlite_open(db_path)?;
let ids = opencode_sqlite_all_session_ids(&conn, limit_sessions)?;
let mut out = String::new();
for id in ids {
for line in opencode_sqlite_session_envelope_lines(&conn, db_path, &id)? {
out.push_str(&line);
out.push('\n');
}
}
Ok(out)
}
fn non_empty_lines(text: &str) -> impl Iterator<Item = &str> {
text.lines().map(str::trim).filter(|l| !l.is_empty())
}
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
}
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());
}
}
}
_ => {}
}
}
const UNCONVERTIBLE_IMAGE_MARKER: &str =
"[image: source not captured — unsupported/unconvertible image reference]";
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}"))
}
fn push_str_field(buf: &mut String, s: &str) {
if !buf.is_empty() {
buf.push('\n');
}
buf.push_str(s);
}
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());
}
}
}
}
}
const SUPERCODE_CODEX_PROVENANCE_KEY: &str = "_supercode_codex_provenance";
fn codex_provenance_kind(record: &Value) -> Option<&str> {
match record.get("type").and_then(Value::as_str) {
Some("session_meta") => Some("session_meta"),
Some("turn_context") => Some("turn_context"),
Some("compacted") => Some("compacted"),
Some("event_msg") => match record
.get("payload")
.and_then(|payload| payload.get("type"))
.and_then(Value::as_str)
{
Some("thread_rolled_back") => Some("event_msg/thread_rolled_back"),
Some("thread_goal_updated") => Some("event_msg/thread_goal_updated"),
Some("entered_review_mode") => Some("event_msg/entered_review_mode"),
Some("exited_review_mode") => Some("event_msg/exited_review_mode"),
_ => None,
},
_ => None,
}
}
fn capture_codex_provenance_record(
meta: &mut SessionMeta,
record_index: usize,
raw_line: &str,
record: &Value,
) {
let Some(kind) = codex_provenance_kind(record) else {
return;
};
meta.codex_provenance.push(serde_json::json!({
"record_index": record_index,
"kind": kind,
"raw": raw_line,
}));
}
fn codex_provenance_envelope(meta: &SessionMeta) -> Option<Value> {
(!meta.codex_provenance.is_empty()).then(|| {
serde_json::json!({
"version": 1,
"records": &meta.codex_provenance,
})
})
}
fn restore_codex_provenance(extension: &Value, meta: &mut SessionMeta) -> Result<bool> {
if extension.get("version").and_then(Value::as_u64) != Some(1) {
return Err(Error::InvalidSession(
"invalid portable Codex provenance: expected version 1".to_string(),
));
}
let Some(records) = extension.get("records").and_then(Value::as_array) else {
return Err(Error::InvalidSession(
"invalid portable Codex provenance: `records` must be an array".to_string(),
));
};
if records.is_empty() {
return Err(Error::InvalidSession(
"invalid portable Codex provenance: `records` must not be empty".to_string(),
));
}
let mut restored = Vec::with_capacity(records.len());
for entry in records {
let Some(_record_index) = entry.get("record_index").and_then(Value::as_u64) else {
return Err(Error::InvalidSession(
"invalid portable Codex provenance: record_index must be an integer".to_string(),
));
};
let Some(kind) = entry.get("kind").and_then(Value::as_str) else {
return Err(Error::InvalidSession(
"invalid portable Codex provenance: kind must be a string".to_string(),
));
};
let Some(raw) = entry.get("raw").and_then(Value::as_str) else {
return Err(Error::InvalidSession(
"invalid portable Codex provenance: raw must be a string".to_string(),
));
};
let Ok(record) = serde_json::from_str::<Value>(raw) else {
return Err(Error::InvalidSession(
"invalid portable Codex provenance: raw is not valid JSON".to_string(),
));
};
if codex_provenance_kind(&record) != Some(kind) {
return Err(Error::InvalidSession(format!(
"invalid portable Codex provenance: kind `{kind}` does not match raw record"
)));
}
restored.push(entry.clone());
}
meta.codex_provenance = restored;
meta.codex_headers.clear();
for entry in &meta.codex_provenance {
let Some(raw) = entry.get("raw").and_then(Value::as_str) else {
continue;
};
let Ok(record) = serde_json::from_str::<Value>(raw) else {
continue;
};
if matches!(
record.get("type").and_then(Value::as_str),
Some("session_meta") | Some("turn_context")
) {
meta.codex_headers.push(record);
}
}
Ok(true)
}
fn restore_codex_provenance_from_top_level(record: &Value, meta: &mut SessionMeta) -> Result<bool> {
match record.get(SUPERCODE_CODEX_PROVENANCE_KEY) {
Some(extension) => restore_codex_provenance(extension, meta),
None => Ok(false),
}
}
fn inject_first_jsonl_top_level(out: &mut String, key: &str, extension: Value) {
let Some(line_end) = out.find('\n') else {
return;
};
let Ok(mut record) = serde_json::from_str::<Value>(&out[..line_end]) else {
return;
};
let Some(object) = record.as_object_mut() else {
return;
};
object.insert(key.to_string(), extension);
out.replace_range(..line_end, &record.to_string());
}
fn inject_codex_provenance(out: &mut String, extension: Value) {
let Some(line_end) = out.find('\n') else {
return;
};
let Ok(mut record) = serde_json::from_str::<Value>(&out[..line_end]) else {
return;
};
if record.get("type").and_then(Value::as_str) != Some("session_meta") {
return;
}
let Some(payload) = record.get_mut("payload").and_then(Value::as_object_mut) else {
return;
};
payload.insert(SUPERCODE_CODEX_PROVENANCE_KEY.to_string(), extension);
out.replace_range(..line_end, &record.to_string());
}
fn remove_last_turn(messages: &mut Vec<ChatMessage>) {
if let Some(idx) = messages.iter().rposition(|m| m.role == Role::User) {
messages.truncate(idx);
} else {
messages.clear();
}
if let Some(last) = messages.last_mut() {
last.metadata.remove("__codex_open_turn");
}
}
fn truncate_messages(messages: &mut Vec<ChatMessage>, message_limit: usize) {
truncate_messages_with_anchor(messages, message_limit, Vec::new());
}
fn truncate_messages_with_anchor(
messages: &mut Vec<ChatMessage>,
message_limit: usize,
preceding_users: Vec<ChatMessage>,
) {
let limit = message_limit.max(1);
let anchor_limit = limit.min(2);
let mut anchor_indices = messages
.iter()
.enumerate()
.rev()
.filter_map(|(index, message)| (message.role == Role::User).then_some(index))
.take(anchor_limit)
.collect::<Vec<_>>();
anchor_indices.reverse();
let needed_preceding = anchor_limit.saturating_sub(anchor_indices.len());
let mut preceding_anchors = preceding_users
.into_iter()
.rev()
.take(needed_preceding)
.collect::<Vec<_>>();
preceding_anchors.reverse();
if messages.len() + preceding_anchors.len() <= limit {
preceding_anchors.append(messages);
*messages = preceding_anchors;
return;
}
if messages.len() <= limit && preceding_anchors.is_empty() {
return;
}
let anchor_count = anchor_indices.len() + preceding_anchors.len();
let target_index_count = limit.saturating_sub(preceding_anchors.len());
let mut selected_indices = anchor_indices.clone();
for index in (0..messages.len()).rev() {
if selected_indices.len() >= target_index_count || anchor_indices.contains(&index) {
continue;
}
selected_indices.push(index);
}
selected_indices.sort_unstable();
let mut selected = Vec::with_capacity(limit);
selected.append(&mut preceding_anchors);
selected.extend(
selected_indices
.into_iter()
.map(|index| messages[index].clone()),
);
debug_assert_eq!(selected.len(), limit.max(anchor_count));
*messages = selected;
}
fn truncate_session_messages(session: &mut Session, message_limit: usize) {
truncate_messages(&mut session.messages, message_limit);
}
fn agent_message_text(payload: &Value) -> String {
match payload.get("message") {
Some(Value::String(s)) => s.clone(),
Some(other) => extract_text_content(Some(other)),
None => String::new(),
}
}
fn collect_codex_assistant_texts(jsonl: &str) -> std::collections::HashSet<String> {
let mut set = std::collections::HashSet::new();
for line in non_empty_lines(jsonl) {
let Ok(v) = serde_json::from_str::<Value>(line) else {
continue;
};
if v.get("type").and_then(Value::as_str) != Some("response_item") {
continue;
}
let payload = v.get("payload").unwrap_or(&Value::Null);
if payload.get("type").and_then(Value::as_str) == Some("message")
&& payload.get("role").and_then(Value::as_str) == Some("assistant")
{
let text = extract_text_content(payload.get("content"));
if !text.trim().is_empty() {
set.insert(text.trim().to_string());
}
}
}
set
}
fn capture_codex_session_meta(payload: &Value, meta: &mut SessionMeta) {
if meta.session_id.is_none() {
if let Some(id) = payload.get("id").and_then(Value::as_str) {
meta.session_id = Some(id.to_string());
}
}
if meta.cwd.is_none() {
if let Some(cwd) = payload.get("cwd").and_then(Value::as_str) {
meta.cwd = Some(PathBuf::from(cwd));
}
}
if meta.system_prompt.is_none() {
let bi = payload.get("base_instructions");
let text = match bi {
Some(Value::String(s)) => Some(s.clone()),
Some(Value::Object(_)) => bi
.and_then(|b| b.get("text"))
.and_then(Value::as_str)
.map(str::to_string),
_ => None,
};
meta.system_prompt = text;
}
if meta.model.is_none() {
if let Some(m) = payload.get("model").and_then(Value::as_str) {
meta.model = Some(m.to_string());
}
}
let mut put = |key: &str, v: Option<&Value>| {
if let Some(s) = v.and_then(Value::as_str) {
meta.lineage.insert(key.to_string(), s.to_string());
}
};
put("parent_thread_id", payload.get("parent_thread_id"));
put("forked_from_id", payload.get("forked_from_id"));
put("thread_source", payload.get("thread_source"));
if !meta.lineage.contains_key("claude_fork_context_ref_raw") {
if let Some(v) = payload.get("claude_fork_context_ref") {
meta.lineage
.insert("claude_fork_context_ref_raw".to_string(), v.to_string());
}
}
if let Some(spawn) = payload
.get("source")
.and_then(|s| s.get("subagent"))
.and_then(|s| s.get("thread_spawn"))
{
if let Some(p) = spawn.get("parent_thread_id").and_then(Value::as_str) {
meta.lineage
.insert("parent_thread_id".to_string(), p.to_string());
}
for k in ["agent_role", "agent_nickname"] {
if let Some(s) = spawn.get(k).and_then(Value::as_str) {
meta.lineage.insert(k.to_string(), s.to_string());
}
}
if let Some(d) = spawn.get("depth").and_then(Value::as_i64) {
meta.lineage.insert("depth".to_string(), d.to_string());
}
}
}
fn depth_of(mut i: usize, parent_of: &[Option<usize>]) -> usize {
let mut d = 0;
let mut guard = 0;
while let Some(p) = parent_of[i] {
if p == i || guard > parent_of.len() {
break;
}
i = p;
d += 1;
guard += 1;
}
d
}
fn codex_turn_id(payload: &Value) -> Option<&str> {
payload
.get("metadata")
.and_then(|m| m.get("turn_id"))
.and_then(Value::as_str)
}
fn collect_codex_group_ids_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(payload) = v.get("payload") {
if let Some(tid) = codex_turn_id(payload) {
ids.insert(tid.to_string());
}
}
}
}
ids
}
fn stamp_new_codex_messages(messages: &mut [ChatMessage], from: usize, ts: Option<&str>) {
let Some(ts) = ts else { return };
let Some(slice) = messages.get_mut(from..) else {
return;
};
for m in slice {
m.metadata
.entry("timestamp".to_string())
.or_insert_with(|| ts.to_string());
}
}
fn push_codex_item(payload: &Value, out: &mut Vec<ChatMessage>) {
match payload.get("type").and_then(Value::as_str) {
Some("message") => {
let role = match payload.get("role").and_then(Value::as_str) {
Some("user") => Role::User,
Some("assistant") => Role::Assistant,
_ => Role::System,
};
let content = payload.get("content");
let text = extract_text_content(content);
let images = codex_extract_images(content);
let is_empty_assistant =
role == Role::Assistant && text.trim().is_empty() && images.is_empty();
if !text.trim().is_empty() || !images.is_empty() || is_empty_assistant {
let content_parts = if images.is_empty() {
None
} else {
let mut parts = Vec::new();
if !text.trim().is_empty() {
parts.push(serde_json::json!({"type": "text", "text": text.clone()}));
}
parts.extend(images);
Some(parts)
};
let mut msg = ChatMessage {
role,
content: if content_parts.is_some() || text.is_empty() {
None
} else {
Some(text)
},
content_parts,
tool_calls: None,
tool_call_id: None,
name: None,
metadata: Default::default(),
};
if role == Role::Assistant {
if let Some(phase) = payload.get("phase").and_then(Value::as_str) {
msg.metadata.insert("phase".to_string(), phase.to_string());
}
msg.metadata
.insert("__codex_open_turn".to_string(), "true".to_string());
}
if let Some(tid) = codex_turn_id(payload) {
msg.metadata.insert("turn_id".to_string(), tid.to_string());
}
if role == Role::System {
if let Some(subtype) = payload
.get("metadata")
.and_then(|m| m.get("claude_system_subtype"))
.and_then(Value::as_str)
{
msg.metadata
.insert("systemSubtype".to_string(), subtype.to_string());
}
}
if is_empty_assistant {
msg.metadata
.insert("empty_assistant_record".to_string(), "true".to_string());
}
out.push(msg);
}
}
Some("function_call") => {
let id = payload
.get("call_id")
.and_then(Value::as_str)
.unwrap_or_default();
let raw_name = payload
.get("name")
.and_then(Value::as_str)
.unwrap_or_default();
let qualified;
let name = match payload.get("namespace").and_then(Value::as_str) {
Some(ns) if !ns.is_empty() && !raw_name.starts_with(ns) => {
qualified = format!("{ns}__{raw_name}");
qualified.as_str()
}
_ => raw_name,
};
let args = payload
.get("arguments")
.map(value_to_arg_string)
.unwrap_or_else(|| "{}".to_string());
let call = function_call(id, name, args);
let can_merge = out.last().is_some_and(|last| {
last.role == Role::Assistant
&& last.metadata.contains_key("__codex_open_turn")
&& match codex_turn_id(payload) {
Some(fc_tid) => {
last.metadata.get("turn_id").map(String::as_str) == Some(fc_tid)
}
None => true,
}
});
if can_merge {
out.last_mut()
.expect("can_merge implies out.last() is Some")
.tool_calls
.get_or_insert_with(Vec::new)
.push(call);
} else {
push_assistant(out, String::new(), vec![call]);
if let Some(tid) = codex_turn_id(payload) {
if let Some(last) = out.last_mut() {
last.metadata
.insert("__codex_open_turn".to_string(), "true".to_string());
last.metadata.insert("turn_id".to_string(), tid.to_string());
}
}
}
}
Some("function_call_output") => {
let id = payload
.get("call_id")
.and_then(Value::as_str)
.unwrap_or_default();
let result = match payload.get("output") {
Some(Value::String(s)) => s.clone(),
Some(v) => extract_text_content(Some(v)),
None => String::new(),
};
let mut message = tool_message(id, result);
crate::mark_tool_outcome_unknown(&mut message);
out.push(message);
}
Some("custom_tool_call") => {
let id = payload
.get("call_id")
.and_then(Value::as_str)
.unwrap_or_default();
let name = payload
.get("name")
.and_then(Value::as_str)
.unwrap_or_default();
let args = payload
.get("input")
.map(Value::to_string)
.unwrap_or_else(|| "{}".to_string());
push_assistant(out, String::new(), vec![function_call(id, name, args)]);
if let Some(message) = out.last_mut() {
message.metadata.insert(
"codex_custom_tool_call_ids".to_string(),
serde_json::json!([id]).to_string(),
);
}
}
Some("custom_tool_call_output") => {
let id = payload
.get("call_id")
.and_then(Value::as_str)
.unwrap_or_default();
let result = match payload.get("output") {
Some(Value::String(s)) => s.clone(),
Some(v) => extract_text_content(Some(v)),
None => String::new(),
};
let mut message = tool_message(id, result);
crate::mark_tool_outcome_unknown(&mut message);
out.push(message);
}
Some("tool_search_call") => {
let id = payload
.get("call_id")
.and_then(Value::as_str)
.unwrap_or_default();
let args = payload
.get("arguments")
.map(value_to_arg_string)
.unwrap_or_else(|| "{}".to_string());
let call = function_call(id, "tool_search", args);
let can_merge = out.last().is_some_and(|last| {
last.role == Role::Assistant
&& last.metadata.contains_key("__codex_open_turn")
&& match codex_turn_id(payload) {
Some(fc_tid) => {
last.metadata.get("turn_id").map(String::as_str) == Some(fc_tid)
}
None => true,
}
});
if can_merge {
out.last_mut()
.expect("can_merge implies out.last() is Some")
.tool_calls
.get_or_insert_with(Vec::new)
.push(call);
} else {
push_assistant(out, String::new(), vec![call]);
if let Some(tid) = codex_turn_id(payload) {
if let Some(last) = out.last_mut() {
last.metadata
.insert("__codex_open_turn".to_string(), "true".to_string());
last.metadata.insert("turn_id".to_string(), tid.to_string());
}
}
}
}
Some("tool_search_output") => {
let id = payload
.get("call_id")
.and_then(Value::as_str)
.unwrap_or_default();
let result = payload
.get("tools")
.map(value_to_arg_string)
.unwrap_or_default();
out.push(tool_message(id, result));
}
Some("web_search_call") => {
push_assistant(out, "[web_search]".to_string(), Vec::new());
}
Some("image_generation_call") => {
let prompt = payload
.get("revised_prompt")
.and_then(Value::as_str)
.unwrap_or("");
push_assistant(
out,
format!("[image_generation] {prompt}").trim().to_string(),
Vec::new(),
);
}
_ => {}
}
}
const SUPERCODE_GEMINI_MESSAGE_KEY: &str = "_supercode_gemini_message";
fn set_gemini_message_extension(value: &mut Value, message: &ChatMessage) {
value[SUPERCODE_GEMINI_MESSAGE_KEY] = serde_json::json!({
"schema": 1,
"role": message.role,
"content": message.content,
"content_parts": message.content_parts,
"tool_calls": message.tool_calls,
"tool_call_id": message.tool_call_id,
"name": message.name,
"metadata": message.metadata,
});
}
fn restore_gemini_message_extension(value: &Value, message: &mut ChatMessage) {
let Some(extension) = value.get(SUPERCODE_GEMINI_MESSAGE_KEY) else {
return;
};
if extension.get("schema").and_then(Value::as_u64) != Some(1) {
return;
}
if let Some(role) = extension
.get("role")
.and_then(|value| serde_json::from_value(value.clone()).ok())
{
message.role = role;
}
message.content = extension
.get("content")
.and_then(Value::as_str)
.map(str::to_string);
message.content_parts = extension
.get("content_parts")
.and_then(|value| serde_json::from_value(value.clone()).ok());
message.tool_calls = extension
.get("tool_calls")
.and_then(|value| serde_json::from_value(value.clone()).ok());
message.tool_call_id = extension
.get("tool_call_id")
.and_then(Value::as_str)
.map(str::to_string);
message.name = extension
.get("name")
.and_then(Value::as_str)
.map(str::to_string);
message.metadata.clear();
if let Some(metadata) = extension.get("metadata").and_then(Value::as_object) {
for (key, value) in metadata {
if let Some(value) = value.as_str() {
message.metadata.insert(key.clone(), value.to_string());
}
}
}
}
fn capture_grok_scalar_metadata(value: &Value, message: &mut ChatMessage, keys: &[&str]) {
for key in keys {
if let Some(value) = value.get(*key) {
message.metadata.insert(
format!("grok_{key}"),
value
.as_str()
.map(str::to_string)
.unwrap_or_else(|| value.to_string()),
);
}
}
}
fn grok_human_user_text(raw: &str) -> Option<String> {
let text = raw.trim();
if text.is_empty() || text.starts_with("<user_info>") || text.starts_with("<system-reminder>") {
return None;
}
let unwrapped = text
.strip_prefix("<user_query>")
.and_then(|value| value.strip_suffix("</user_query>"))
.map(str::trim)
.unwrap_or(text);
(!unwrapped.is_empty()).then(|| unwrapped.to_string())
}
const SUPERCODE_GROK_MESSAGE_KEY: &str = "_supercode_grok_message";
const SUPERCODE_TOOL_OUTCOME_KEY: &str = "_supercode_tool_outcome";
fn restore_tool_outcome_extension(value: &Value, message: &mut ChatMessage) {
if !crate::is_tool_error(message)
&& value
.get(SUPERCODE_TOOL_OUTCOME_KEY)
.and_then(Value::as_str)
== Some("unknown")
{
crate::mark_tool_outcome_unknown(message);
}
}
fn grok_message_extension(source: SessionSource, message: &ChatMessage) -> Option<Value> {
let metadata = message
.metadata
.iter()
.filter(|(key, _)| {
key.starts_with("gemini_") || key.starts_with("grok_") || key.starts_with("goose_")
})
.map(|(key, value)| (key.clone(), Value::String(value.clone())))
.collect::<serde_json::Map<_, _>>();
let has_portable_fields =
!metadata.is_empty() || message.metadata.contains_key("codex_custom_tool_call_ids");
(matches!(
source,
SessionSource::Gemini | SessionSource::Grok | SessionSource::Goose
) || has_portable_fields
|| message.content_parts.is_some())
.then(|| {
serde_json::json!({
"schema": 2,
"role": message.role,
"content": message.content,
"content_parts": message.content_parts,
"tool_calls": message.tool_calls,
"tool_call_id": message.tool_call_id,
"name": message.name,
"metadata": message.metadata,
})
})
}
fn set_grok_target_message_extension(value: &mut Value, message: &ChatMessage) {
value[SUPERCODE_GROK_MESSAGE_KEY] = serde_json::json!({
"schema": 2,
"role": message.role,
"content": message.content,
"content_parts": message.content_parts,
"tool_calls": message.tool_calls,
"tool_call_id": message.tool_call_id,
"name": message.name,
"metadata": message.metadata,
});
}
fn set_grok_message_extension(value: &mut Value, source: SessionSource, message: &ChatMessage) {
if let Some(extension) = grok_message_extension(source, message) {
value[SUPERCODE_GROK_MESSAGE_KEY] = extension;
}
}
fn restore_grok_message_extension(value: &Value, message: &mut ChatMessage) {
let Some(extension) = value.get(SUPERCODE_GROK_MESSAGE_KEY) else {
return;
};
let codex_open_turn = message.metadata.get("__codex_open_turn").cloned();
let codex_turn_id = message.metadata.get("turn_id").cloned();
let extension_has_turn_id = extension
.get("metadata")
.and_then(Value::as_object)
.is_some_and(|metadata| metadata.contains_key("turn_id"));
if extension.get("schema").and_then(Value::as_u64) == Some(2) {
if let Some(role) = extension
.get("role")
.and_then(|value| serde_json::from_value(value.clone()).ok())
{
message.role = role;
}
message.content = extension
.get("content")
.and_then(Value::as_str)
.map(str::to_string);
message.content_parts = extension
.get("content_parts")
.and_then(|value| serde_json::from_value(value.clone()).ok());
message.tool_call_id = extension
.get("tool_call_id")
.and_then(Value::as_str)
.map(str::to_string);
message.name = extension
.get("name")
.and_then(Value::as_str)
.map(str::to_string);
message.metadata.clear();
}
if let Some(metadata) = extension.get("metadata").and_then(Value::as_object) {
for (key, value) in metadata {
if let Some(value) = value.as_str() {
message.metadata.insert(key.clone(), value.to_string());
}
}
}
if let Some(name) = extension.get("name").and_then(Value::as_str) {
message.name = Some(name.to_string());
}
if let Some(marker) = codex_open_turn {
message
.metadata
.insert("__codex_open_turn".to_string(), marker);
}
if let Some(turn_id) = codex_turn_id {
message.metadata.insert("turn_id".to_string(), turn_id);
if !extension_has_turn_id {
message.metadata.insert(
"__grok_remove_synthetic_turn_id".to_string(),
"true".to_string(),
);
}
}
}
fn restore_single_grok_message(value: &Value, messages: &mut [ChatMessage]) {
if let [message] = messages {
restore_grok_message_extension(value, message);
}
}
fn normalize_goose_message(native: &Value, native_index: usize, out: &mut Vec<ChatMessage>) {
let role = match native.get("role").and_then(Value::as_str) {
Some("assistant") => Role::Assistant,
_ => Role::User,
};
let created = native.get("created").and_then(Value::as_i64);
let native_id = native.get("id").and_then(Value::as_str);
let mut text = Vec::new();
let mut content_parts = Vec::new();
let mut tool_calls = Vec::new();
let mut tool_results = Vec::new();
for (block_index, block) in native
.get("content")
.and_then(Value::as_array)
.into_iter()
.flatten()
.enumerate()
{
match block.get("type").and_then(Value::as_str) {
Some("text") => {
if let Some(value) = block.get("text").and_then(Value::as_str) {
text.push(value.to_string());
content_parts.push(serde_json::json!({"type": "text", "text": value}));
}
}
Some("image") => {
let data = block
.get("data")
.and_then(Value::as_str)
.unwrap_or_default();
let media_type = block
.get("mimeType")
.or_else(|| block.get("mime_type"))
.and_then(Value::as_str)
.unwrap_or("application/octet-stream");
content_parts.push(serde_json::json!({
"type": "image_url",
"image_url": {"url": format!("data:{media_type};base64,{data}")},
}));
}
Some("toolRequest" | "frontendToolRequest") => {
let id = block
.get("id")
.and_then(Value::as_str)
.map(str::to_string)
.unwrap_or_else(|| format!("goose-{native_index}-{block_index}"));
let call = block
.get("toolCall")
.and_then(|call| {
(call.get("status").and_then(Value::as_str) == Some("success"))
.then(|| call.get("value"))
.flatten()
})
.or_else(|| block.get("toolCall"));
let Some(call) = call else { continue };
let name = call.get("name").and_then(Value::as_str).unwrap_or("tool");
let arguments = call
.get("arguments")
.map(value_to_arg_string)
.unwrap_or_else(|| "{}".to_string());
tool_calls.push(function_call(&id, name, arguments));
}
Some("toolResponse") => tool_results.push(block.clone()),
_ => {}
}
}
if !text.is_empty() || !content_parts.is_empty() || !tool_calls.is_empty() {
let has_non_text = content_parts
.iter()
.any(|part| part.get("type").and_then(Value::as_str) != Some("text"));
let mut message = ChatMessage {
role,
content: (!text.is_empty()).then(|| text.join("\n")),
content_parts: has_non_text.then_some(content_parts),
tool_calls: (!tool_calls.is_empty()).then_some(tool_calls),
tool_call_id: None,
name: None,
metadata: Default::default(),
};
capture_goose_message_metadata(native, created, native_id, &mut message);
out.push(message);
}
for (result_index, block) in tool_results.into_iter().enumerate() {
let id = block
.get("id")
.and_then(Value::as_str)
.map(str::to_string)
.unwrap_or_else(|| format!("goose-{native_index}-result-{result_index}"));
let result = block.get("toolResult").unwrap_or(&Value::Null);
let status_error = result.get("status").and_then(Value::as_str) == Some("error");
let value = result.get("value").unwrap_or(result);
let is_error = status_error || value.get("isError").and_then(Value::as_bool) == Some(true);
let output = if status_error {
result
.get("error")
.and_then(Value::as_str)
.unwrap_or("Goose tool call failed")
.to_string()
} else {
value
.get("content")
.and_then(Value::as_array)
.map(|content| {
content
.iter()
.filter_map(|part| {
part.get("text")
.and_then(Value::as_str)
.map(str::to_string)
.or_else(|| Some(part.to_string()))
})
.collect::<Vec<_>>()
.join("\n")
})
.unwrap_or_else(|| value.to_string())
};
let mut message = tool_message(&id, output);
if is_error {
crate::mark_tool_error(&mut message);
}
capture_goose_message_metadata(native, created, native_id, &mut message);
out.push(message);
}
}
fn capture_goose_message_metadata(
native: &Value,
created: Option<i64>,
native_id: Option<&str>,
message: &mut ChatMessage,
) {
if let Some(created) = created {
message
.metadata
.insert("goose_created".to_string(), created.to_string());
}
if let Some(native_id) = native_id {
message
.metadata
.insert("goose_message_id".to_string(), native_id.to_string());
}
if let Some(metadata) = native.get("metadata") {
message
.metadata
.insert("goose_metadata".to_string(), metadata.to_string());
}
}
#[doc(hidden)]
pub fn percent_decode_path(encoded: &str) -> Option<String> {
fn hex(byte: u8) -> Option<u8> {
match byte {
b'0'..=b'9' => Some(byte - b'0'),
b'a'..=b'f' => Some(byte - b'a' + 10),
b'A'..=b'F' => Some(byte - b'A' + 10),
_ => None,
}
}
let bytes = encoded.as_bytes();
let mut decoded = Vec::with_capacity(bytes.len());
let mut index = 0usize;
while index < bytes.len() {
if bytes[index] == b'%' {
let high = *bytes.get(index + 1)?;
let low = *bytes.get(index + 2)?;
decoded.push(hex(high)? * 16 + hex(low)?);
index += 3;
} else {
decoded.push(bytes[index]);
index += 1;
}
}
String::from_utf8(decoded).ok()
}
fn capture_pi_header(v: &Value, meta: &mut SessionMeta) -> Result<()> {
restore_codex_provenance_from_top_level(v, meta)?;
if let Some(id) = v.get("id").and_then(Value::as_str) {
meta.session_id = Some(id.to_string());
}
if let Some(cwd) = v.get("cwd").and_then(Value::as_str) {
meta.cwd = Some(PathBuf::from(cwd));
}
let version = v
.get("version")
.and_then(Value::as_u64)
.map(|n| n.to_string())
.unwrap_or_else(|| "1".to_string());
meta.lineage.insert("pi_version".to_string(), version);
if let Some(ts) = v.get("timestamp").and_then(Value::as_str) {
meta.lineage
.insert("created_at".to_string(), ts.to_string());
}
if let Some(ps) = v.get("parentSession").and_then(Value::as_str) {
meta.lineage
.insert("parent_session_path".to_string(), ps.to_string());
}
if !meta.lineage.contains_key("claude_fork_context_ref_raw") {
if let Some(v) = v.get("claude_fork_context_ref") {
meta.lineage
.insert("claude_fork_context_ref_raw".to_string(), v.to_string());
}
}
Ok(())
}
fn pi_image_shape(item: &Value) -> Option<(String, String)> {
let mime = item.get("mimeType").and_then(Value::as_str)?;
let data = item.get("data").and_then(Value::as_str)?;
if mime.is_empty() || data.is_empty() {
return None;
}
if !data
.bytes()
.all(|b| b.is_ascii_alphanumeric() || matches!(b, b'+' | b'/' | b'='))
{
return None;
}
Some((mime.to_string(), data.to_string()))
}
#[doc(hidden)]
pub fn pi_content_has_unknown_image_shape(content: Option<&Value>) -> bool {
let Some(Value::Array(items)) = content else {
return false;
};
items.iter().any(|item| {
item.get("type").and_then(Value::as_str) == Some("image") && pi_image_shape(item).is_none()
})
}
fn pi_content_to_text_and_parts(content: Option<&Value>) -> (String, Option<Vec<Value>>, bool) {
match content {
Some(Value::String(s)) => (s.clone(), None, false),
Some(Value::Array(items)) => {
let mut text = String::new();
let mut parts: Vec<Value> = Vec::new();
let mut has_image = false;
let mut unknown_image_shape = false;
for item in items {
match item.get("type").and_then(Value::as_str) {
Some("text") => {
if let Some(t) = item.get("text").and_then(Value::as_str) {
push_str_field(&mut text, t);
}
}
Some("image") => {
has_image = true;
match pi_image_shape(item) {
Some((mime, data)) => {
parts.push(serde_json::json!({
"type": "image_url",
"image_url": {"url": format!("data:{mime};base64,{data}")},
}));
}
None => unknown_image_shape = true,
}
}
_ => {}
}
}
if unknown_image_shape {
return (String::new(), None, true);
}
if has_image {
if !text.trim().is_empty() {
parts.insert(0, serde_json::json!({"type": "text", "text": text.clone()}));
}
(text, Some(parts), false)
} else {
(text, None, false)
}
}
_ => (String::new(), None, false),
}
}
fn push_pi_user(msg_v: &Value, out: &mut Vec<ChatMessage>) {
let (text, parts, unknown_image_shape) = pi_content_to_text_and_parts(msg_v.get("content"));
if unknown_image_shape {
return;
}
if text.trim().is_empty() && parts.is_none() {
return;
}
let mut msg = match parts {
Some(parts) => ChatMessage {
role: Role::User,
content: None,
content_parts: Some(parts),
tool_calls: None,
tool_call_id: None,
name: None,
metadata: Default::default(),
},
None => ChatMessage::user(text),
};
if let Some(ts) = msg_v.get("timestamp").and_then(Value::as_u64) {
msg.metadata
.insert("pi_msg_timestamp".to_string(), ts.to_string());
}
out.push(msg);
}
fn push_pi_assistant(msg_v: &Value, out: &mut Vec<ChatMessage>) {
let mut text = String::new();
let mut calls: Vec<ToolCall> = Vec::new();
let mut thinking = String::new();
let mut thinking_seen = false;
let mut thinking_sig: Option<String> = None;
let mut thinking_redacted = false;
let mut text_sig: Option<String> = None;
let mut thought_sig: Option<String> = None;
if let Some(Value::Array(blocks)) = msg_v.get("content") {
for b in blocks {
match b.get("type").and_then(Value::as_str) {
Some("text") => {
if let Some(t) = b.get("text").and_then(Value::as_str) {
push_str_field(&mut text, t);
}
if let Some(sig) = b.get("textSignature") {
text_sig = Some(match sig {
Value::String(s) => s.clone(),
other => other.to_string(),
});
}
}
Some("thinking") => {
thinking_seen = true;
if let Some(t) = b.get("thinking").and_then(Value::as_str) {
push_str_field(&mut thinking, t);
}
if let Some(sig) = b.get("thinkingSignature").and_then(Value::as_str) {
thinking_sig = Some(sig.to_string());
}
if b.get("redacted").and_then(Value::as_bool) == Some(true) {
thinking_redacted = true;
}
}
Some("toolCall") => {
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("arguments")
.cloned()
.unwrap_or_else(|| Value::Object(Default::default()));
calls.push(function_call(id, name, args.to_string()));
if let Some(sig) = b.get("thoughtSignature").and_then(Value::as_str) {
thought_sig = Some(sig.to_string());
}
}
_ => {}
}
}
}
let before = out.len();
push_assistant(out, text, calls);
let is_empty_error =
!thinking_seen && msg_v.get("stopReason").and_then(Value::as_str) == Some("error");
if out.len() == before && !is_empty_error {
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_seen {
empty
.metadata
.insert("empty_assistant_record".to_string(), "true".to_string());
}
out.push(empty);
}
if out.len() > before {
let msg = out.last_mut().expect("just pushed");
if thinking_seen {
msg.metadata.insert("thinking".to_string(), thinking);
}
if let Some(s) = thinking_sig {
msg.metadata.insert("thinking_signature".to_string(), s);
}
if thinking_redacted {
msg.metadata
.insert("pi_thinking_redacted".to_string(), "true".to_string());
}
if let Some(s) = text_sig {
msg.metadata.insert("pi_text_signature".to_string(), s);
}
if let Some(s) = thought_sig {
msg.metadata.insert("pi_thought_signature".to_string(), s);
}
for (key, field) in [
("pi_api", "api"),
("pi_provider", "provider"),
("pi_response_model", "responseModel"),
("pi_response_id", "responseId"),
("pi_stop_reason", "stopReason"),
("pi_error_message", "errorMessage"),
] {
if let Some(s) = msg_v.get(field).and_then(Value::as_str) {
msg.metadata.insert(key.to_string(), s.to_string());
}
}
if let Some(diag) = msg_v.get("diagnostics") {
if !diag.is_null() {
msg.metadata
.insert("pi_diagnostics".to_string(), diag.to_string());
}
}
if let Some(usage) = msg_v.get("usage") {
if !usage.is_null() {
msg.metadata
.insert("pi_usage".to_string(), usage.to_string());
}
}
if let Some(ts) = msg_v.get("timestamp").and_then(Value::as_u64) {
msg.metadata
.insert("pi_msg_timestamp".to_string(), ts.to_string());
}
}
}
fn push_pi_tool_result(msg_v: &Value, out: &mut Vec<ChatMessage>) {
let id = msg_v
.get("toolCallId")
.and_then(Value::as_str)
.unwrap_or_default();
let name = msg_v
.get("toolName")
.and_then(Value::as_str)
.unwrap_or_default();
let (text, parts, unknown_image_shape) = pi_content_to_text_and_parts(msg_v.get("content"));
if unknown_image_shape {
return;
}
let mut msg = ChatMessage {
role: Role::Tool,
content: Some(text),
content_parts: parts,
tool_calls: None,
tool_call_id: Some(id.to_string()),
name: Some(name.to_string()),
metadata: Default::default(),
};
if let Some(details) = msg_v.get("details") {
if !details.is_null() {
msg.metadata
.insert("pi_tool_details".to_string(), details.to_string());
}
}
let is_error = msg_v
.get("isError")
.and_then(Value::as_bool)
.unwrap_or(false);
msg.metadata
.insert("pi_is_error".to_string(), is_error.to_string());
if is_error {
crate::mark_tool_error(&mut msg);
}
if let Some(ts) = msg_v.get("timestamp").and_then(Value::as_u64) {
msg.metadata
.insert("pi_msg_timestamp".to_string(), ts.to_string());
}
out.push(msg);
}
fn push_pi_bash(msg_v: &Value, out: &mut Vec<ChatMessage>) {
let command = msg_v.get("command").and_then(Value::as_str).unwrap_or("");
let output = msg_v.get("output").and_then(Value::as_str).unwrap_or("");
let exit_code = msg_v.get("exitCode").and_then(Value::as_i64);
let cancelled = msg_v
.get("cancelled")
.and_then(Value::as_bool)
.unwrap_or(false);
let truncated = msg_v
.get("truncated")
.and_then(Value::as_bool)
.unwrap_or(false);
let mut text = format!("$ {command}\n{output}");
if let Some(code) = exit_code {
if code != 0 {
text.push_str(&format!("\n[exit code: {code}]"));
}
}
if cancelled {
text.push_str("\n[cancelled]");
}
if truncated {
text.push_str("\n[truncated]");
}
let mut msg = ChatMessage::user(text);
msg.metadata
.insert("pi_bash_command".to_string(), command.to_string());
msg.metadata
.insert("pi_bash_output".to_string(), output.to_string());
if let Some(code) = exit_code {
msg.metadata
.insert("pi_bash_exit_code".to_string(), code.to_string());
}
msg.metadata
.insert("pi_bash_cancelled".to_string(), cancelled.to_string());
msg.metadata
.insert("pi_bash_truncated".to_string(), truncated.to_string());
if let Some(p) = msg_v.get("fullOutputPath").and_then(Value::as_str) {
msg.metadata
.insert("pi_bash_full_output_path".to_string(), p.to_string());
}
if msg_v.get("excludeFromContext").and_then(Value::as_bool) == Some(true) {
msg.metadata
.insert("pi_exclude_from_context".to_string(), "true".to_string());
}
if let Some(ts) = msg_v.get("timestamp").and_then(Value::as_u64) {
msg.metadata
.insert("pi_msg_timestamp".to_string(), ts.to_string());
}
out.push(msg);
}
const PI_CLAUDE_SYSTEM_CUSTOM_TYPE: &str = "supercode_claude_system";
fn push_pi_custom_common(v: &Value, out: &mut Vec<ChatMessage>) {
if v.get("customType").and_then(Value::as_str) == Some(PI_CLAUDE_SYSTEM_CUSTOM_TYPE) {
let content = v.get("content").and_then(Value::as_str).unwrap_or("");
if content.trim().is_empty() {
return;
}
let subtype = v
.get("details")
.and_then(|d| d.get("claude_system_subtype"))
.and_then(Value::as_str)
.unwrap_or("local_command");
out.push(ChatMessage::system(content.to_string()).with_meta("systemSubtype", subtype));
return;
}
let (text, parts, unknown_image_shape) = pi_content_to_text_and_parts(v.get("content"));
if unknown_image_shape {
return;
}
if text.trim().is_empty() && parts.is_none() {
return;
}
let mut msg = match parts {
Some(parts) => ChatMessage {
role: Role::User,
content: None,
content_parts: Some(parts),
tool_calls: None,
tool_call_id: None,
name: None,
metadata: Default::default(),
},
None => ChatMessage::user(text),
};
if let Some(ct) = v.get("customType").and_then(Value::as_str) {
msg.metadata
.insert("pi_custom_type".to_string(), ct.to_string());
}
if let Some(d) = v.get("display").and_then(Value::as_bool) {
msg.metadata.insert("pi_display".to_string(), d.to_string());
}
if let Some(details) = v.get("details") {
if !details.is_null() {
msg.metadata
.insert("pi_details".to_string(), details.to_string());
}
}
if let Some(ts) = v.get("timestamp").and_then(Value::as_u64) {
msg.metadata
.insert("pi_msg_timestamp".to_string(), ts.to_string());
}
out.push(msg);
}
fn push_pi_compaction(entry_v: &Value, out: &mut Vec<ChatMessage>) {
let summary = entry_v.get("summary").and_then(Value::as_str).unwrap_or("");
if summary.trim().is_empty() {
return;
}
let mut msg = ChatMessage::user(format!("[compaction summary]\n{summary}"));
msg.metadata
.insert("pi_type".to_string(), "compaction".to_string());
if let Some(fk) = entry_v.get("firstKeptEntryId").and_then(Value::as_str) {
msg.metadata
.insert("pi_first_kept_entry_id".to_string(), fk.to_string());
}
if let Some(tb) = entry_v.get("tokensBefore").and_then(Value::as_u64) {
msg.metadata
.insert("pi_tokens_before".to_string(), tb.to_string());
}
if let Some(d) = entry_v.get("details") {
if !d.is_null() {
msg.metadata.insert("pi_details".to_string(), d.to_string());
}
}
if entry_v.get("fromHook").and_then(Value::as_bool) == Some(true) {
msg.metadata
.insert("pi_from_hook".to_string(), "true".to_string());
}
out.push(msg);
}
fn push_pi_branch_summary(entry_v: &Value, out: &mut Vec<ChatMessage>) {
let summary = entry_v.get("summary").and_then(Value::as_str).unwrap_or("");
if summary.trim().is_empty() {
return;
}
let mut msg = ChatMessage::user(format!("[branch summary]\n{summary}"));
msg.metadata
.insert("pi_type".to_string(), "branch_summary".to_string());
if let Some(f) = entry_v.get("fromId").and_then(Value::as_str) {
msg.metadata.insert("pi_from_id".to_string(), f.to_string());
}
if let Some(d) = entry_v.get("details") {
if !d.is_null() {
msg.metadata.insert("pi_details".to_string(), d.to_string());
}
}
if entry_v.get("fromHook").and_then(Value::as_bool) == Some(true) {
msg.metadata
.insert("pi_from_hook".to_string(), "true".to_string());
}
out.push(msg);
}
pub const OPENCODE_COMPACTED_TOOL_PLACEHOLDER: &str = "[Old tool result content cleared]";
fn capture_opencode_session_info(si: &Value, meta: &mut SessionMeta) -> Result<()> {
restore_codex_provenance_from_top_level(si, meta)?;
if let Some(id) = si.get("id").and_then(Value::as_str) {
meta.session_id = Some(id.to_string());
}
if let Some(dir) = si.get("directory").and_then(Value::as_str) {
meta.cwd = Some(PathBuf::from(dir));
}
if let Some(agent) = si.get("agent").and_then(Value::as_str) {
meta.agent_id = Some(agent.to_string());
}
if let Some(model) = si.get("model") {
let provider = model.get("providerID").and_then(Value::as_str);
let id = model.get("id").and_then(Value::as_str);
if let (Some(p), Some(i)) = (provider, id) {
meta.model = Some(format!("{p}/{i}"));
}
}
if let Some(project_id) = si.get("projectID").and_then(Value::as_str) {
meta.lineage
.insert("projectID".to_string(), project_id.to_string());
}
if let Some(slug) = si.get("slug").and_then(Value::as_str) {
meta.lineage.insert("slug".to_string(), slug.to_string());
}
if let Some(ws) = si.get("workspaceID").and_then(Value::as_str) {
meta.lineage
.insert("workspaceID".to_string(), ws.to_string());
}
if let Some(parent) = si.get("parentID").and_then(Value::as_str) {
meta.lineage
.insert("parent_session_id".to_string(), parent.to_string());
meta.lineage
.insert("parent_thread_id".to_string(), parent.to_string());
}
if !meta.lineage.contains_key("claude_fork_context_ref_raw") {
if let Some(v) = si.get("claude_fork_context_ref") {
meta.lineage
.insert("claude_fork_context_ref_raw".to_string(), v.to_string());
}
}
Ok(())
}
#[doc(hidden)]
pub fn opencode_file_image_part(part: &Value) -> Option<Value> {
let mime = part.get("mime").and_then(Value::as_str)?;
let url = part.get("url").and_then(Value::as_str)?;
if !mime.starts_with("image/") || !url.starts_with("data:") {
return None;
}
Some(serde_json::json!({"type": "image_url", "image_url": {"url": url}}))
}
const OPENCODE_CLAUDE_SYSTEM_SUBTYPE_KEY: &str = "supercode_claude_system_subtype";
fn opencode_claude_system_subtype(parts: &[Value]) -> Option<String> {
let [part] = parts else { return None };
if part.get("type").and_then(Value::as_str) != Some("text") {
return None;
}
if part.get("synthetic").and_then(Value::as_bool) != Some(true) {
return None;
}
part.get("metadata")
.and_then(|m| m.get(OPENCODE_CLAUDE_SYSTEM_SUBTYPE_KEY))
.and_then(Value::as_str)
.map(str::to_string)
}
fn push_opencode_claude_system(
msg_value: &Value,
parts: &[Value],
subtype: String,
out: &mut Vec<ChatMessage>,
) {
let Some(text) = parts
.first()
.and_then(|p| p.get("text"))
.and_then(Value::as_str)
else {
return;
};
if text.trim().is_empty() {
return;
}
let mut msg = ChatMessage::system(text.to_string()).with_meta("systemSubtype", subtype);
set_opencode_msg_timestamp(&mut msg, msg_value);
out.push(msg);
}
fn set_opencode_msg_timestamp(msg: &mut ChatMessage, msg_value: &Value) {
if let Some(ms) = msg_value
.get("time")
.and_then(|t| t.get("created"))
.and_then(Value::as_i64)
{
msg.metadata
.insert("timestamp".to_string(), crate::sidecar::ms_to_rfc3339(ms));
}
}
fn push_opencode_user(
msg_value: &Value,
parts: &[Value],
out: &mut Vec<ChatMessage>,
meta: &mut SessionMeta,
first_system_seen: &mut bool,
) {
let mut text = String::new();
let mut image_parts: Vec<Value> = Vec::new();
let mut has_ignored = false;
for p in parts {
match p.get("type").and_then(Value::as_str) {
Some("text") => {
if p.get("ignored").and_then(Value::as_bool) == Some(true) {
has_ignored = true;
continue; }
if let Some(t) = p.get("text").and_then(Value::as_str) {
push_str_field(&mut text, t);
}
}
Some("file") => {
if let Some(img) = opencode_file_image_part(p) {
image_parts.push(img);
}
}
_ => {}
}
}
let has_images = !image_parts.is_empty();
if text.trim().is_empty() && !has_images {
return;
}
let mut msg = if has_images {
let mut all = Vec::new();
if !text.trim().is_empty() {
all.push(serde_json::json!({"type": "text", "text": text.clone()}));
}
all.extend(image_parts);
ChatMessage {
role: Role::User,
content: None,
content_parts: Some(all),
tool_calls: None,
tool_call_id: None,
name: None,
metadata: Default::default(),
}
} else {
ChatMessage::user(text)
};
if has_ignored {
msg.metadata
.insert("oc_has_ignored_part".to_string(), "true".to_string());
}
if let Some(id) = msg_value.get("id").and_then(Value::as_str) {
msg.metadata
.insert("oc_message_id".to_string(), id.to_string());
}
if let Some(agent) = msg_value.get("agent").and_then(Value::as_str) {
msg.metadata.insert("agent".to_string(), agent.to_string());
}
if let Some(model) = msg_value.get("model") {
if !model.is_null() {
msg.metadata.insert("model".to_string(), model.to_string());
}
}
if let Some(system) = msg_value.get("system").and_then(Value::as_str) {
if !*first_system_seen {
meta.system_prompt = Some(system.to_string());
*first_system_seen = true;
}
msg.metadata
.insert("system".to_string(), system.to_string());
}
for p in parts {
if p.get("type").and_then(Value::as_str) == Some("compaction") {
msg.metadata
.insert("phase".to_string(), "compaction".to_string());
if let Some(t) = p.get("tail_start_id").and_then(Value::as_str) {
msg.metadata
.insert("tail_start_id".to_string(), t.to_string());
}
}
}
set_opencode_msg_timestamp(&mut msg, msg_value);
restore_grok_message_extension(msg_value, &mut msg);
out.push(msg);
}
fn push_opencode_assistant(
msg_value: &Value,
parts: &[Value],
out: &mut Vec<ChatMessage>,
meta: &mut SessionMeta,
) {
let mut text = String::new();
let mut calls: Vec<ToolCall> = Vec::new();
let mut thinking = String::new();
let mut reasoning_seen = false;
let mut thinking_sig: Option<String> = None;
let mut tool_results: Vec<(String, String, Value)> = Vec::new();
for p in parts {
match p.get("type").and_then(Value::as_str) {
Some("text") => {
if p.get("ignored").and_then(Value::as_bool) == Some(true) {
continue;
}
if let Some(t) = p.get("text").and_then(Value::as_str) {
push_str_field(&mut text, t);
}
}
Some("reasoning") => {
reasoning_seen = true;
if let Some(t) = p.get("text").and_then(Value::as_str) {
push_str_field(&mut thinking, t);
}
if let Some(sig) = p
.get("metadata")
.and_then(|m| m.get("anthropic"))
.and_then(|a| a.get("signature"))
.and_then(Value::as_str)
{
thinking_sig = Some(sig.to_string());
}
}
Some("tool") => {
let call_id = p.get("callID").and_then(Value::as_str).unwrap_or_default();
let tool_name = p.get("tool").and_then(Value::as_str).unwrap_or_default();
let status = p
.get("state")
.and_then(|s| s.get("status"))
.and_then(Value::as_str);
let known_status = matches!(
status,
Some("pending") | Some("running") | Some("completed") | Some("error")
);
if call_id.is_empty() || !known_status {
continue;
}
let input = p
.get("state")
.and_then(|s| s.get("input"))
.cloned()
.unwrap_or_else(|| Value::Object(Default::default()));
calls.push(function_call(call_id, tool_name, input.to_string()));
if matches!(status, Some("completed") | Some("error")) {
tool_results.push((call_id.to_string(), tool_name.to_string(), p.clone()));
}
}
_ => {}
}
}
let before = out.len();
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 !reasoning_seen {
empty
.metadata
.insert("empty_assistant_record".to_string(), "true".to_string());
}
out.push(empty);
}
if out.len() > before {
let msg = out.last_mut().expect("just pushed");
if reasoning_seen {
msg.metadata.insert("thinking".to_string(), thinking);
}
if let Some(sig) = thinking_sig {
msg.metadata.insert("thinking_signature".to_string(), sig);
}
if let Some(id) = msg_value.get("id").and_then(Value::as_str) {
msg.metadata
.insert("oc_message_id".to_string(), id.to_string());
}
if let Some(agent) = msg_value.get("agent").and_then(Value::as_str) {
msg.metadata.insert("agent".to_string(), agent.to_string());
if meta.agent_id.is_none() {
meta.agent_id = Some(agent.to_string());
}
}
let provider = msg_value.get("providerID").and_then(Value::as_str);
let model_id = msg_value.get("modelID").and_then(Value::as_str);
if let (Some(p), Some(i)) = (provider, model_id) {
let full = format!("{p}/{i}");
msg.metadata.insert("model".to_string(), full.clone());
if meta.model.is_none() {
meta.model = Some(full);
}
}
if let Some(cwd) = msg_value
.get("path")
.and_then(|p| p.get("cwd"))
.and_then(Value::as_str)
{
if meta.cwd.is_none() {
meta.cwd = Some(PathBuf::from(cwd));
}
}
if msg_value.get("summary").and_then(Value::as_bool) == Some(true) {
msg.metadata
.insert("is_summary".to_string(), "true".to_string());
}
for (key, field) in [
("finish", "finish"),
("variant", "variant"),
("mode", "mode"),
] {
if let Some(s) = msg_value.get(field).and_then(Value::as_str) {
msg.metadata.insert(key.to_string(), s.to_string());
}
}
for (key, field) in [
("cost", "cost"),
("tokens", "tokens"),
("error", "error"),
("structured", "structured"),
] {
if let Some(v) = msg_value.get(field) {
if !v.is_null() {
msg.metadata.insert(key.to_string(), v.to_string());
}
}
}
for p in parts {
if p.get("type").and_then(Value::as_str) == Some("tool")
&& p.get("tool").and_then(Value::as_str) == Some("task")
{
if let (Some(call_id), Some(child)) = (
p.get("callID").and_then(Value::as_str),
p.get("metadata")
.and_then(|m| m.get("sessionId"))
.and_then(Value::as_str),
) {
msg.metadata.insert(
format!("oc_task_child_session_id__{call_id}"),
child.to_string(),
);
}
}
}
set_opencode_msg_timestamp(msg, msg_value);
restore_grok_message_extension(msg_value, msg);
}
for (call_id, tool_name, part) in tool_results {
let status = part
.get("state")
.and_then(|s| s.get("status"))
.and_then(Value::as_str);
let compacted_at = part
.get("state")
.and_then(|s| s.get("time"))
.and_then(|t| t.get("compacted"))
.and_then(Value::as_i64);
let real_output = part
.get("state")
.and_then(|s| s.get("output"))
.and_then(Value::as_str)
.unwrap_or("")
.to_string();
let (content, is_error) = match status {
Some("completed") => {
if compacted_at.is_some() {
(OPENCODE_COMPACTED_TOOL_PLACEHOLDER.to_string(), false)
} else {
(real_output.clone(), false)
}
}
Some("error") => {
let err = part
.get("state")
.and_then(|s| s.get("error"))
.and_then(Value::as_str)
.unwrap_or("")
.to_string();
(err, true)
}
_ => (String::new(), false),
};
let mut tmsg = ChatMessage {
role: Role::Tool,
content: Some(content),
content_parts: None,
tool_calls: None,
tool_call_id: Some(call_id),
name: Some(tool_name),
metadata: Default::default(),
};
if let Some(original_position) = part
.get(OPENCODE_SUPERCODE_RESULT_POSITION)
.and_then(Value::as_u64)
{
tmsg.metadata.insert(
OPENCODE_INTERNAL_ORIGINAL_POSITION.to_string(),
original_position.to_string(),
);
}
if is_error {
crate::mark_tool_error(&mut tmsg);
}
restore_tool_outcome_extension(&part, &mut tmsg);
if let Some(ts) = compacted_at {
tmsg.metadata
.insert("oc_tool_output_compacted".to_string(), real_output);
tmsg.metadata
.insert("oc_tool_time_compacted".to_string(), ts.to_string());
}
if status == Some("completed") {
if let Some(atts) = part
.get("state")
.and_then(|s| s.get("attachments"))
.and_then(Value::as_array)
{
let images: Vec<Value> = atts.iter().filter_map(opencode_file_image_part).collect();
if !images.is_empty() {
let mut parts = Vec::new();
if let Some(t) = &tmsg.content {
if !t.is_empty() {
parts.push(serde_json::json!({"type": "text", "text": t}));
}
}
parts.extend(images);
tmsg.content_parts = Some(parts);
}
}
}
if let Some(id) = part.get("id").and_then(Value::as_str) {
tmsg.metadata
.insert("oc_part_id".to_string(), id.to_string());
}
let tool_ts = part
.get("state")
.and_then(|s| s.get("time"))
.and_then(|t| t.get("end").or_else(|| t.get("start")))
.and_then(Value::as_i64);
if let Some(ms) = tool_ts {
tmsg.metadata
.insert("timestamp".to_string(), crate::sidecar::ms_to_rfc3339(ms));
}
restore_grok_message_extension(&part, &mut tmsg);
out.push(tmsg);
}
}
fn push_text(buf: &mut String, v: Option<&Value>) {
if let Some(Value::String(s)) = v {
if !buf.is_empty() {
buf.push('\n');
}
buf.push_str(s);
}
}
fn extract_tool_result_content(
content: Option<&Value>,
tool_use_result: Option<&Value>,
) -> (String, Vec<Value>) {
let mut parts: Vec<String> = Vec::new();
let mut images: Vec<Value> = Vec::new();
match content {
Some(Value::String(s)) => {
if !s.is_empty() {
parts.push(s.clone());
}
}
Some(Value::Array(items)) => {
for item in items {
match item.get("type").and_then(Value::as_str) {
Some("text") => {
if let Some(t) = item.get("text").and_then(Value::as_str) {
parts.push(t.to_string());
}
}
Some("image") => match claude_image_block_to_part(item) {
Some(part) => images.push(part),
None => parts.push(UNCONVERTIBLE_IMAGE_MARKER.to_string()),
},
Some("tool_reference") => {
let name = item.get("tool_name").and_then(Value::as_str).unwrap_or("?");
parts.push(format!("[tool_reference: {name}]"));
}
_ => {
if let Some(s) = item.as_str() {
parts.push(s.to_string());
}
}
}
}
}
Some(other) => parts.push(other.to_string()),
None => {}
}
let joined = parts.join("\n");
if !joined.trim().is_empty() || !images.is_empty() {
return (joined, images);
}
match tool_use_result {
Some(Value::String(s)) => (s.clone(), images),
Some(v) => (v.to_string(), images),
None => (joined, images),
}
}
fn extract_text_content(v: Option<&Value>) -> String {
match v {
Some(Value::String(s)) => s.clone(),
Some(Value::Array(items)) => {
let mut parts = Vec::new();
for item in items {
if let Some(t) = item.get("text").and_then(Value::as_str) {
parts.push(t.to_string());
} else if let Some(s) = item.as_str() {
parts.push(s.to_string());
}
}
parts.join("\n")
}
Some(other) => other.to_string(),
None => String::new(),
}
}
fn codex_extract_images(content: Option<&Value>) -> Vec<Value> {
let Some(Value::Array(items)) = content else {
return Vec::new();
};
items
.iter()
.filter(|item| item.get("type").and_then(Value::as_str) == Some("input_image"))
.filter_map(|item| {
let url = item.get("image_url").and_then(Value::as_str)?;
if url.is_empty() {
return None;
}
Some(serde_json::json!({"type": "image_url", "image_url": {"url": url}}))
})
.collect()
}
fn value_to_arg_string(v: &Value) -> String {
match v {
Value::String(s) => s.clone(),
other => other.to_string(),
}
}
fn push_gemini_user_parts(
messages: &mut Vec<ChatMessage>,
content_parts: Vec<Value>,
timestamp: Option<&str>,
source: &Value,
) {
if content_parts.is_empty() {
return;
}
let mut message = ChatMessage {
role: Role::User,
content: None,
content_parts: Some(content_parts),
tool_calls: None,
tool_call_id: None,
name: None,
metadata: Default::default(),
};
if let Some(timestamp) = timestamp {
message
.metadata
.insert("timestamp".into(), timestamp.into());
}
restore_gemini_message_extension(source, &mut message);
messages.push(message);
}
fn function_call(id: &str, name: &str, arguments: String) -> ToolCall {
ToolCall {
id: id.to_string(),
kind: "function".to_string(),
function: FunctionCall {
name: name.to_string(),
arguments,
},
}
}
fn tool_message(tool_call_id: &str, content: String) -> ChatMessage {
ChatMessage {
role: Role::Tool,
content: Some(content),
content_parts: None,
tool_calls: None,
tool_call_id: Some(tool_call_id.to_string()),
name: None,
metadata: Default::default(),
}
}
fn push_assistant(out: &mut Vec<ChatMessage>, text: String, calls: Vec<ToolCall>) {
let has_text = !text.trim().is_empty();
if !has_text && calls.is_empty() {
return;
}
out.push(ChatMessage {
role: Role::Assistant,
content: has_text.then_some(text),
content_parts: None,
tool_calls: (!calls.is_empty()).then_some(calls),
tool_call_id: None,
name: None,
metadata: Default::default(),
});
}
const SYNTH_TS: &str = "2026-01-01T00:00:00.000Z";
const SYNTH_TS_MS: i64 = 1_767_225_600_000;
fn msg_timestamp_or_synth(msg: &ChatMessage) -> &str {
match msg.metadata.get("timestamp") {
Some(ts) if crate::sidecar::rfc3339_to_ms(ts).is_some() => ts.as_str(),
_ => SYNTH_TS,
}
}
fn opencode_message_timestamp(msg: &ChatMessage, cursor: &mut i64) -> Result<i64> {
if let Some(real) = msg
.metadata
.get("timestamp")
.and_then(|s| crate::sidecar::rfc3339_to_ms(s))
{
if msg.metadata.contains_key("supercode_native_uuid") && real <= *cursor {
*cursor = cursor.checked_add(1).ok_or_else(|| {
crate::Error::Other(
"cannot synthesize an OpenCode continuation timestamp after i64::MAX"
.to_string(),
)
})?;
return Ok(*cursor);
}
*cursor = (*cursor).max(real);
return Ok(real);
}
let next = cursor.checked_add(1).ok_or_else(|| {
crate::Error::Other(
"cannot synthesize an OpenCode continuation timestamp after i64::MAX".to_string(),
)
})?;
*cursor = next.max(SYNTH_TS_MS);
Ok(*cursor)
}
fn opencode_max_timestamp(value: &Value) -> Option<i64> {
fn max_number(value: &Value) -> Option<i64> {
match value {
Value::Number(n) => n.as_i64(),
Value::Array(values) => values.iter().filter_map(max_number).max(),
Value::Object(fields) => fields.values().filter_map(max_number).max(),
_ => None,
}
}
match value {
Value::Array(values) => values.iter().filter_map(opencode_max_timestamp).max(),
Value::Object(fields) => fields
.iter()
.filter_map(|(key, value)| {
if key == "time" {
max_number(value)
} else {
opencode_max_timestamp(value)
}
})
.max(),
_ => None,
}
}
fn msg_pi_native_timestamp_ms(msg: &ChatMessage) -> i64 {
msg.metadata
.get("pi_msg_timestamp")
.and_then(|s| s.parse::<i64>().ok())
.unwrap_or(SYNTH_TS_MS)
}
fn synth_uuid(n: usize) -> String {
format!("00000000-0000-4000-8000-{n:012x}")
}
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
}
fn push_jsonl(out: &mut String, value: &Value) {
out.push_str(&value.to_string());
out.push('\n');
}
fn push_spliced_line(out: &mut String, line: &str, new_id: Option<&str>, key: &str) {
if let Some(new_id) = new_id {
if let Ok(mut v) = serde_json::from_str::<Value>(line) {
if v.get(key).is_some() {
v[key] = Value::String(new_id.to_string());
out.push_str(&v.to_string());
out.push('\n');
return;
}
}
}
out.push_str(line);
out.push('\n');
}
impl Session {
fn cwd_string(&self) -> String {
self.meta
.cwd
.as_ref()
.map(|p| p.to_string_lossy().into_owned())
.unwrap_or_else(|| ".".to_string())
}
fn spliced_prefix_lens(&self) -> (usize, usize) {
let message_prefix_len = self
.imported_message_count
.unwrap_or(self.messages.len())
.min(self.messages.len());
let appended_count = self.messages.len() - message_prefix_len;
let raw_prefix_len = self.raw.len().saturating_sub(appended_count);
(raw_prefix_len, message_prefix_len)
}
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 let Some(extension) = codex_provenance_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_CODEX_PROVENANCE_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);
}
}
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
}
fn to_codex_jsonl(&self) -> String {
let mut out = String::new();
if self.meta.codex_headers.is_empty() {
self.write_synthesized_codex_header(&mut out);
} else {
for header in &self.meta.codex_headers {
let mut header = header.clone();
if header.get("type").and_then(Value::as_str) == Some("session_meta") {
if let Some(id) = &self.meta.session_id {
if let Some(payload) = header.get_mut("payload") {
payload["id"] = Value::String(id.clone());
}
}
}
push_jsonl(&mut out, &header);
}
}
self.write_codex_records(&mut out, &self.messages, &std::collections::HashSet::new());
if let Some(extension) = codex_provenance_envelope(&self.meta) {
inject_codex_provenance(&mut out, extension);
}
out
}
fn write_codex_records(
&self,
out: &mut String,
messages: &[ChatMessage],
seed_used_ids: &std::collections::HashSet<String>,
) {
let mut tool_search_call_ids: std::collections::HashSet<String> = Default::default();
let mut next_group_id: u64 = 0;
let mut used_group_ids: std::collections::HashSet<String> = seed_used_ids.clone();
for msg in messages {
if is_replay_excluded(msg) {
continue;
}
let real_turn_id = msg.metadata.get("turn_id").map(String::as_str);
match msg.role {
Role::System => {
let subtype_meta = msg
.metadata
.get("systemSubtype")
.map(|s| ("claude_system_subtype", s.as_str()));
self.push_codex_message(
out,
"developer",
"input_text",
msg,
real_turn_id,
subtype_meta,
)
}
Role::User => {
self.push_codex_message(out, "user", "input_text", msg, real_turn_id, None)
}
Role::Assistant => {
let has_text = msg.content.as_deref().is_some_and(|t| !t.is_empty());
let has_message_record = has_text
|| msg.content_parts.is_some()
|| msg.metadata.contains_key("empty_assistant_record");
let group_id: Option<String> = if let Some(real) = real_turn_id {
if used_group_ids.contains(real) {
let mut n = 1u64;
let mut candidate = format!("{real}~dup{n}");
while used_group_ids.contains(&candidate) {
n += 1;
candidate = format!("{real}~dup{n}");
}
Some(candidate)
} else {
Some(real.to_string())
}
} else if !msg.tool_calls().is_empty() {
let mut candidate = format!("sc-grp-{next_group_id}");
next_group_id += 1;
while used_group_ids.contains(&candidate) {
candidate = format!("sc-grp-{next_group_id}");
next_group_id += 1;
}
Some(candidate)
} else {
None
};
if let Some(g) = &group_id {
used_group_ids.insert(g.clone());
}
if has_message_record {
self.push_codex_message(
out,
"assistant",
"output_text",
msg,
group_id.as_deref(),
None,
);
}
for tc in msg.tool_calls() {
let custom_tool_call = msg
.metadata
.get("codex_custom_tool_call_ids")
.and_then(|raw| serde_json::from_str::<Vec<String>>(raw).ok())
.is_some_and(|ids| ids.iter().any(|id| id == &tc.id));
if custom_tool_call {
let input = tc
.function
.parsed_arguments()
.unwrap_or_else(|_| Value::String(tc.function.arguments.clone()));
let mut payload = with_turn_id(
serde_json::json!({
"type": "custom_tool_call",
"name": tc.function.name,
"input": input,
"call_id": tc.id,
}),
group_id.as_deref(),
);
set_grok_message_extension(&mut payload, self.meta.source, msg);
push_jsonl(
out,
&codex_response_item(payload, msg_timestamp_or_synth(msg)),
);
} else if tc.function.name == "tool_search" {
tool_search_call_ids.insert(tc.id.clone());
let mut payload = with_turn_id(
serde_json::json!({
"type": "tool_search_call",
"arguments": tc.function.arguments,
"call_id": tc.id,
}),
group_id.as_deref(),
);
set_grok_message_extension(&mut payload, self.meta.source, msg);
push_jsonl(
out,
&codex_response_item(payload, msg_timestamp_or_synth(msg)),
);
} else {
let mut payload = with_turn_id(
serde_json::json!({
"type": "function_call",
"name": tc.function.name,
"arguments": tc.function.arguments,
"call_id": tc.id,
}),
group_id.as_deref(),
);
set_grok_message_extension(&mut payload, self.meta.source, msg);
push_jsonl(
out,
&codex_response_item(payload, msg_timestamp_or_synth(msg)),
);
}
}
}
Role::Tool
if msg
.tool_call_id
.as_deref()
.is_some_and(|id| tool_search_call_ids.contains(id)) =>
{
let content = msg.content.clone().unwrap_or_default();
let tools =
serde_json::from_str::<Value>(&content).unwrap_or(Value::String(content));
let mut payload = serde_json::json!({
"type": "tool_search_output",
"call_id": msg.tool_call_id.clone().unwrap_or_default(),
"tools": tools,
});
set_grok_message_extension(&mut payload, self.meta.source, msg);
push_jsonl(
out,
&codex_response_item(payload, msg_timestamp_or_synth(msg)),
);
}
Role::Tool => {
let mut payload = serde_json::json!({
"type": "function_call_output",
"call_id": msg.tool_call_id.clone().unwrap_or_default(),
"output": codex_tool_output_text(msg),
});
set_grok_message_extension(&mut payload, self.meta.source, msg);
push_jsonl(
out,
&codex_response_item(payload, msg_timestamp_or_synth(msg)),
);
}
}
}
}
fn to_codex_jsonl_spliced(&self, session_id: Option<&str>) -> String {
let (raw_prefix_len, message_prefix_len) = self.spliced_prefix_lens();
let mut out = String::new();
for line in &self.raw[..raw_prefix_len] {
match session_id {
Some(id) => {
let patched = serde_json::from_str::<Value>(line)
.ok()
.filter(|v| v.get("type").and_then(Value::as_str) == Some("session_meta"))
.map(|mut v| {
if let Some(payload) = v.get_mut("payload") {
payload["id"] = Value::String(id.to_string());
}
v.to_string()
});
out.push_str(patched.as_deref().unwrap_or(line));
}
None => out.push_str(line),
}
out.push('\n');
}
let mut seed_used_ids = collect_codex_group_ids_from_raw(&self.raw[..raw_prefix_len]);
for msg in &self.messages[..message_prefix_len] {
if let Some(tid) = msg.metadata.get("turn_id") {
seed_used_ids.insert(tid.clone());
}
}
self.write_codex_records(
&mut out,
&self.messages[message_prefix_len..],
&seed_used_ids,
);
out
}
fn write_synthesized_codex_header(&self, out: &mut String) {
let mut meta_payload = serde_json::json!({
"id": self.meta.session_id.clone().unwrap_or_else(|| synth_uuid(0)),
"timestamp": SYNTH_TS,
"cwd": self.cwd_string(),
"originator": "supercode",
"cli_version": env!("CARGO_PKG_VERSION"),
"source": "exec",
"thread_source": "user",
"model_provider": "openai",
});
if let Some(sp) = &self.meta.system_prompt {
meta_payload["base_instructions"] = serde_json::json!({"text": sp});
}
if let Some(raw) = self.meta.lineage.get("claude_fork_context_ref_raw") {
meta_payload["claude_fork_context_ref"] =
serde_json::from_str(raw).unwrap_or_else(|_| Value::String(raw.clone()));
}
push_jsonl(
out,
&serde_json::json!({"timestamp": SYNTH_TS, "type": "session_meta", "payload": meta_payload}),
);
if let Some(model) = &self.meta.model {
push_jsonl(
out,
&serde_json::json!({
"timestamp": SYNTH_TS,
"type": "turn_context",
"payload": {"model": model, "cwd": self.cwd_string()},
}),
);
}
}
fn push_codex_message(
&self,
out: &mut String,
role: &str,
text_type: &str,
msg: &ChatMessage,
turn_id: Option<&str>,
extra_metadata: Option<(&str, &str)>,
) {
let mut payload = with_turn_id(
serde_json::json!({
"type": "message",
"role": role,
"content": codex_message_content_blocks(text_type, msg),
}),
turn_id,
);
if let Some((k, v)) = extra_metadata {
if payload.get("metadata").is_none() {
payload["metadata"] = serde_json::json!({});
}
payload["metadata"][k] = serde_json::json!(v);
}
set_grok_message_extension(&mut payload, self.meta.source, msg);
push_jsonl(
out,
&codex_response_item(payload, msg_timestamp_or_synth(msg)),
);
}
fn to_pi_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();
push_pi_header(
&mut out,
&session_id,
&cwd,
self.meta
.lineage
.get("parent_session_path")
.map(String::as_str),
self.meta.lineage.get("created_at").map(String::as_str),
self.meta
.lineage
.get("claude_fork_context_ref_raw")
.map(String::as_str),
);
let mut used_ids: HashSet<String> = HashSet::new();
let mut counter: u64 = 0;
self.write_pi_entries(&mut out, &self.messages, None, &mut used_ids, &mut counter);
if let Some(extension) = codex_provenance_envelope(&self.meta) {
inject_first_jsonl_top_level(&mut out, SUPERCODE_CODEX_PROVENANCE_KEY, extension);
}
out
}
fn write_pi_entries(
&self,
out: &mut String,
messages: &[ChatMessage],
mut parent: Option<String>,
used_ids: &mut HashSet<String>,
counter: &mut u64,
) {
let mut paired_tool_names = HashMap::<String, String>::new();
for msg in messages {
if is_replay_excluded(msg) {
continue;
}
for call in msg.tool_calls() {
paired_tool_names.insert(call.id.clone(), call.function.name.clone());
}
let id = pi_fresh_id(used_ids, counter);
let mut entry = 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());
serde_json::json!({
"type": "message",
"id": id,
"parentId": parent,
"timestamp": msg_timestamp_or_synth(msg),
"message": {
"role": "custom",
"customType": PI_CLAUDE_SYSTEM_CUSTOM_TYPE,
"content": content,
"display": true,
"details": {"claude_system_subtype": subtype},
"timestamp": msg_pi_native_timestamp_ms(msg),
},
})
}
Role::User => serde_json::json!({
"type": "message",
"id": id,
"parentId": parent,
"timestamp": msg_timestamp_or_synth(msg),
"message": {
"role": "user",
"content": pi_content_value(msg),
"timestamp": msg_pi_native_timestamp_ms(msg),
},
}),
Role::Assistant => {
let api = msg
.metadata
.get("pi_api")
.cloned()
.unwrap_or_else(|| "anthropic-messages".to_string());
let provider = msg
.metadata
.get("pi_provider")
.cloned()
.unwrap_or_else(|| "anthropic".to_string());
let model = self
.meta
.model
.clone()
.unwrap_or_else(|| "unknown".to_string());
let usage = msg
.metadata
.get("pi_usage")
.and_then(|s| serde_json::from_str::<Value>(s).ok())
.unwrap_or_else(default_pi_usage);
let stop_reason = msg
.metadata
.get("pi_stop_reason")
.cloned()
.unwrap_or_else(|| "stop".to_string());
serde_json::json!({
"type": "message",
"id": id,
"parentId": parent,
"timestamp": msg_timestamp_or_synth(msg),
"message": {
"role": "assistant",
"content": pi_assistant_content_value(msg),
"api": api,
"provider": provider,
"model": model,
"usage": usage,
"stopReason": stop_reason,
"timestamp": msg_pi_native_timestamp_ms(msg),
},
})
}
Role::Tool => serde_json::json!({
"type": "message",
"id": id,
"parentId": parent,
"timestamp": msg_timestamp_or_synth(msg),
"message": {
"role": "toolResult",
"toolCallId": msg.tool_call_id.clone().unwrap_or_default(),
"toolName": msg.name.as_deref().or_else(|| {
msg.tool_call_id
.as_deref()
.and_then(|id| paired_tool_names.get(id).map(String::as_str))
}).unwrap_or_default(),
"content": pi_content_value(msg),
"isError": is_tool_error_flag(msg),
"timestamp": msg_pi_native_timestamp_ms(msg),
},
}),
};
if msg.role == Role::Tool && crate::tool_outcome(msg) == crate::ToolOutcome::Unknown {
entry[SUPERCODE_TOOL_OUTCOME_KEY] = Value::String("unknown".to_string());
}
set_grok_message_extension(&mut entry, self.meta.source, msg);
push_jsonl(out, &entry);
parent = Some(id);
if msg.role == Role::Tool {
if let Some(call_id) = msg.tool_call_id.as_deref() {
paired_tool_names.remove(call_id);
}
}
}
}
fn to_pi_jsonl_spliced(&self, session_id: Option<&str>) -> Result<String> {
let (raw_prefix_len, message_prefix_len) = self.spliced_prefix_lens();
if raw_prefix_len == 0 {
return Ok(self.to_pi_jsonl());
}
let mut out = String::new();
let mut used_ids: HashSet<String> = HashSet::new();
let mut leaf: Option<String> = None;
for (i, line) in self.raw[..raw_prefix_len].iter().enumerate() {
if i == 0 {
if let Ok(v) = serde_json::from_str::<Value>(line) {
if v.get("type").and_then(Value::as_str) == Some("session") {
let needs_v3 = v.get("version").and_then(Value::as_u64) != Some(3);
if needs_v3 || session_id.is_some() {
let mut v = v;
v["version"] = serde_json::json!(3);
if let Some(new_id) = session_id {
v["id"] = Value::String(new_id.to_string());
}
out.push_str(&v.to_string());
out.push('\n');
continue;
}
}
}
}
out.push_str(line);
out.push('\n');
if let Ok(v) = serde_json::from_str::<Value>(line) {
if let Some(id) = v.get("id").and_then(Value::as_str) {
used_ids.insert(id.to_string());
leaf = Some(id.to_string());
}
}
}
let mut counter: u64 = 0;
self.write_pi_entries(
&mut out,
&self.messages[message_prefix_len..],
leaf,
&mut used_ids,
&mut counter,
);
Ok(out)
}
fn to_grok_jsonl(&self) -> String {
let mut out = String::new();
if let Some(prompt) = self
.meta
.system_prompt
.as_deref()
.filter(|prompt| !prompt.is_empty())
{
push_jsonl(
&mut out,
&serde_json::json!({
"type": "system",
"content": prompt,
}),
);
}
self.write_grok_records(&mut out, &self.messages);
if let Some(extension) = codex_provenance_envelope(&self.meta) {
if out.is_empty() {
push_jsonl(
&mut out,
&serde_json::json!({"type": "system", "content": ""}),
);
}
inject_first_jsonl_top_level(&mut out, SUPERCODE_CODEX_PROVENANCE_KEY, extension);
}
out
}
fn write_grok_records(&self, out: &mut String, messages: &[ChatMessage]) {
for message in messages {
if is_replay_excluded(message) {
continue;
}
let mut value = match message.role {
Role::System => serde_json::json!({
"type": "user",
"content": [{"type": "text", "text": message.content.clone().unwrap_or_default()}],
"synthetic_reason": "supercode_system_event",
}),
Role::User => {
let mut value = serde_json::json!({
"type": "user",
"content": [{"type": "text", "text": message.content.clone().unwrap_or_default()}],
});
if let Some(object) = value.as_object_mut() {
for (metadata, field) in [
("grok_prompt_index", "prompt_index"),
("grok_prior_turn_interrupt", "prior_turn_interrupt"),
("grok_synthetic_reason", "synthetic_reason"),
] {
if let Some(raw) = message.metadata.get(metadata) {
object.insert(
field.to_string(),
serde_json::from_str(raw)
.unwrap_or_else(|_| Value::String(raw.clone())),
);
}
}
}
value
}
Role::Assistant => {
let calls = message
.tool_calls()
.iter()
.map(|call| {
serde_json::json!({
"id": call.id,
"name": call.function.name,
"arguments": call.function.arguments,
})
})
.collect::<Vec<_>>();
let mut value = serde_json::json!({
"type": "assistant",
"content": message.content.clone().unwrap_or_default(),
"tool_calls": calls,
"model_id": message.metadata.get("grok_model_id")
.or(self.meta.model.as_ref())
.cloned()
.unwrap_or_else(|| "unknown".to_string()),
});
if let Some(object) = value.as_object_mut() {
for (metadata, field) in [
("grok_model_fingerprint", "model_fingerprint"),
("grok_reasoning_effort", "reasoning_effort"),
] {
if let Some(raw) = message.metadata.get(metadata) {
object.insert(field.to_string(), Value::String(raw.clone()));
}
}
}
value
}
Role::Tool => serde_json::json!({
"type": "tool_result",
"tool_call_id": message.tool_call_id.clone().unwrap_or_default(),
"content": message.content.clone().unwrap_or_default(),
}),
};
set_grok_target_message_extension(&mut value, message);
push_jsonl(out, &value);
}
}
fn to_grok_jsonl_spliced(&self) -> String {
let (raw_prefix_len, message_prefix_len) = self.spliced_prefix_lens();
if raw_prefix_len == 0 {
return self.to_grok_jsonl();
}
let mut out = String::new();
for line in &self.raw[..raw_prefix_len] {
out.push_str(line);
out.push('\n');
}
self.write_grok_records(&mut out, &self.messages[message_prefix_len..]);
out
}
fn to_gemini_jsonl(&self) -> String {
let mut out = String::new();
self.write_gemini_header(&mut out, self.meta.session_id.as_deref());
self.write_gemini_records(&mut out, &self.messages);
push_jsonl(
&mut out,
&serde_json::json!({
"$set": {"lastUpdated": SYNTH_TS}
}),
);
out
}
fn write_gemini_header(&self, out: &mut String, session_id: Option<&str>) {
push_jsonl(
out,
&serde_json::json!({
"sessionId": session_id.unwrap_or("supercode-gemini-session"),
"projectHash": self.meta.lineage.get("gemini_project_hash")
.cloned().unwrap_or_else(|| "supercode".to_string()),
"startTime": self.meta.lineage.get("created_at")
.cloned().unwrap_or_else(|| SYNTH_TS.to_string()),
"lastUpdated": self.meta.lineage.get("updated_at")
.cloned().unwrap_or_else(|| SYNTH_TS.to_string()),
"kind": self.meta.lineage.get("gemini_session_kind")
.cloned().unwrap_or_else(|| "main".to_string()),
}),
);
}
fn write_gemini_records(&self, out: &mut String, messages: &[ChatMessage]) {
let mut call_names = HashMap::new();
for (index, message) in messages.iter().enumerate() {
if is_replay_excluded(message) {
continue;
}
let timestamp = message
.metadata
.get("timestamp")
.cloned()
.unwrap_or_else(|| SYNTH_TS.to_string());
match message.role {
Role::System | Role::User => {
let mut parts = Vec::new();
let text = message.content.clone().or_else(|| {
message.content_parts.as_ref().and_then(|parts| {
let text = parts
.iter()
.filter_map(|part| part.get("text").and_then(Value::as_str))
.collect::<Vec<_>>()
.join(" ");
(!text.is_empty()).then_some(text)
})
});
if let Some(text) = text {
let text = if message.role == Role::System {
format!("[System] {text}")
} else {
text
};
parts.push(serde_json::json!({"text": text}));
}
if let Some(content_parts) = &message.content_parts {
for part in content_parts {
let Some(url) = part
.get("image_url")
.and_then(|value| value.get("url"))
.and_then(Value::as_str)
else {
continue;
};
let Some(rest) = url.strip_prefix("data:") else {
continue;
};
let Some((media_type, data)) = rest.split_once(";base64,") else {
continue;
};
parts.push(serde_json::json!({
"inlineData": {"mimeType": media_type, "data": data}
}));
}
}
if !parts.is_empty() {
let mut value = serde_json::json!({
"id": format!("supercode-user-{index}"),
"timestamp": timestamp,
"type": "user",
"content": parts,
});
set_gemini_message_extension(&mut value, message);
push_jsonl(out, &value);
}
}
Role::Assistant => {
let mut tool_calls = Vec::new();
for call in message.tool_calls() {
call_names.insert(call.id.clone(), call.function.name.clone());
let args = serde_json::from_str::<Value>(&call.function.arguments)
.unwrap_or_else(|_| Value::String(call.function.arguments.clone()));
tool_calls.push(serde_json::json!({
"id": call.id,
"name": call.function.name,
"args": args,
}));
}
let mut value = serde_json::json!({
"id": format!("supercode-gemini-{index}"),
"timestamp": timestamp,
"type": "gemini",
"content": message.content.clone().unwrap_or_default(),
"model": message.metadata.get("gemini_model")
.or(self.meta.model.as_ref())
.cloned().unwrap_or_else(|| "unknown".to_string()),
});
if !tool_calls.is_empty() {
value["toolCalls"] = Value::Array(tool_calls);
}
if let Some(thoughts) = message.metadata.get("gemini_thoughts") {
value["thoughts"] = serde_json::from_str(thoughts)
.unwrap_or_else(|_| Value::String(thoughts.clone()));
}
set_gemini_message_extension(&mut value, message);
push_jsonl(out, &value);
}
Role::Tool => {
let id = message.tool_call_id.clone().unwrap_or_default();
let name = message
.name
.clone()
.or_else(|| call_names.get(&id).cloned())
.unwrap_or_else(|| "tool".to_string());
let output = message.content.clone().unwrap_or_else(|| {
message
.content_parts
.as_ref()
.map(|parts| Value::Array(parts.clone()))
.map(|value| value.to_string())
.unwrap_or_default()
});
let mut value = serde_json::json!({
"id": format!("supercode-tool-{index}"),
"timestamp": timestamp,
"type": "user",
"content": [{
"functionResponse": {
"id": id,
"name": name,
"response": {"output": output}
}
}],
});
set_gemini_message_extension(&mut value, message);
push_jsonl(out, &value);
}
}
}
}
fn to_gemini_jsonl_spliced(&self, session_id: Option<&str>) -> String {
let (raw_prefix_len, message_prefix_len) = self.spliced_prefix_lens();
if raw_prefix_len == 0 {
let mut out = String::new();
self.write_gemini_header(&mut out, session_id.or(self.meta.session_id.as_deref()));
self.write_gemini_records(&mut out, &self.messages);
return out;
}
let mut out = String::new();
for (index, line) in self.raw[..raw_prefix_len].iter().enumerate() {
if index == 0 && session_id.is_some() {
if let Ok(mut value) = serde_json::from_str::<Value>(line) {
if value.get("type").is_none() && value.get("sessionId").is_some() {
value["sessionId"] =
Value::String(session_id.unwrap_or_default().to_string());
push_jsonl(&mut out, &value);
continue;
}
}
}
out.push_str(line);
out.push('\n');
}
self.write_gemini_records(&mut out, &self.messages[message_prefix_len..]);
out
}
fn to_goose_json(&self) -> String {
if self.meta.source == SessionSource::Goose
&& !self.raw.is_empty()
&& self.imported_message_count == Some(self.messages.len())
{
return join_lines_verbatim(&self.raw, self.raw_trailing_newline);
}
self.synthesized_goose_document(None, &self.messages)
}
fn to_goose_json_spliced(&self, session_id: Option<&str>) -> String {
let message_prefix_len = self
.imported_message_count
.unwrap_or(self.messages.len())
.min(self.messages.len());
if self.meta.source == SessionSource::Goose && !self.raw.is_empty() {
if session_id.is_none() && message_prefix_len == self.messages.len() {
return join_lines_verbatim(&self.raw, self.raw_trailing_newline);
}
let source = join_lines_verbatim(&self.raw, self.raw_trailing_newline);
if let Ok(mut document) = serde_json::from_str::<Value>(&source) {
if let Some(session_id) = session_id {
document["id"] = Value::String(session_id.to_string());
}
let appended = self.goose_conversation(&self.messages[message_prefix_len..]);
if let Some(conversation) = document
.get_mut("conversation")
.and_then(Value::as_array_mut)
{
conversation.extend(appended);
document["message_count"] = Value::from(conversation.len());
}
return serde_json::to_string_pretty(&document).unwrap_or_else(|_| {
self.synthesized_goose_document(session_id, &self.messages)
});
}
}
self.synthesized_goose_document(session_id, &self.messages)
}
fn synthesized_goose_document(
&self,
session_id: Option<&str>,
messages: &[ChatMessage],
) -> String {
let mut document = self
.meta
.goose_header
.clone()
.or_else(|| {
self.messages.iter().find_map(|message| {
message
.metadata
.get("goose_session_header")
.and_then(|value| serde_json::from_str(value).ok())
})
})
.unwrap_or_else(|| {
serde_json::json!({
"id": "supercode-goose-session",
"working_dir": self.cwd_string(),
"name": "supercode export",
"user_set_name": false,
"session_type": "user",
"created_at": SYNTH_TS,
"updated_at": SYNTH_TS,
"extension_data": {},
"usage": {},
"accumulated_usage": {},
"accumulated_cost": Value::Null,
"schedule_id": Value::Null,
"recipe": Value::Null,
"user_recipe_values": Value::Null,
"message_count": 0,
"last_message_at": Value::Null,
"provider_name": Value::Null,
"model_config": Value::Null,
"goose_mode": "auto",
"archived_at": Value::Null,
"project_id": Value::Null,
"parent_session_id": Value::Null,
"last_message_snippet": Value::Null,
})
});
document["id"] = Value::String(
session_id
.map(str::to_string)
.or_else(|| self.meta.session_id.clone())
.unwrap_or_else(|| "supercode-goose-session".to_string()),
);
document["working_dir"] = Value::String(self.cwd_string());
let conversation = self.goose_conversation(messages);
document["message_count"] = Value::from(conversation.len());
document["conversation"] = Value::Array(conversation);
serde_json::to_string_pretty(&document).unwrap_or_else(|_| "{}".to_string())
}
fn goose_conversation(&self, messages: &[ChatMessage]) -> Vec<Value> {
let mut out = Vec::new();
let mut last_native_index: Option<String> = None;
let mut tool_names = HashMap::<String, String>::new();
for (index, message) in messages.iter().enumerate() {
if is_replay_excluded(message) {
continue;
}
if let Some(native_index) = message.metadata.get("goose_native_index") {
if last_native_index.as_ref() == Some(native_index) {
continue;
}
last_native_index = Some(native_index.clone());
if let Some(native) = message
.metadata
.get("goose_native_message")
.and_then(|value| serde_json::from_str::<Value>(value).ok())
{
out.push(native);
continue;
}
} else {
last_native_index = None;
}
for call in message.tool_calls() {
tool_names.insert(call.id.clone(), call.function.name.clone());
}
let created = message
.metadata
.get("goose_created")
.and_then(|value| value.parse::<i64>().ok())
.unwrap_or(SYNTH_TS_MS / 1000 + index as i64);
let role = match message.role {
Role::Assistant => "assistant",
_ => "user",
};
let mut content = Vec::new();
if message.role != Role::Tool {
if let Some(text) = &message.content {
let text = if message.role == Role::System {
format!("[System] {text}")
} else {
text.clone()
};
content.push(serde_json::json!({"type": "text", "text": text}));
}
if let Some(parts) = &message.content_parts {
for part in parts {
if let Some(text) = part.get("text").and_then(Value::as_str) {
if message.content.is_none() {
content.push(serde_json::json!({"type": "text", "text": text}));
}
}
let Some(url) = part
.get("image_url")
.and_then(|image| image.get("url"))
.and_then(Value::as_str)
else {
continue;
};
let Some(data) = url.strip_prefix("data:") else {
continue;
};
let Some((media_type, data)) = data.split_once(";base64,") else {
continue;
};
content.push(serde_json::json!({
"type": "image",
"data": data,
"mimeType": media_type,
}));
}
}
}
for call in message.tool_calls() {
let arguments = serde_json::from_str::<Value>(&call.function.arguments)
.unwrap_or_else(|_| Value::String(call.function.arguments.clone()));
content.push(serde_json::json!({
"type": "toolRequest",
"id": call.id,
"toolCall": {
"status": "success",
"value": {"name": call.function.name, "arguments": arguments}
}
}));
}
if message.role == Role::Tool {
let id = message.tool_call_id.clone().unwrap_or_default();
let output = message.content.clone().unwrap_or_else(|| {
message
.content_parts
.as_ref()
.map(|parts| Value::Array(parts.clone()).to_string())
.unwrap_or_default()
});
let tool_result = if crate::is_tool_error(message) {
serde_json::json!({"status": "error", "error": output})
} else {
serde_json::json!({
"status": "success",
"value": {
"content": [{"type": "text", "text": output}],
"isError": false
}
})
};
content.push(serde_json::json!({
"type": "toolResponse",
"id": id,
"toolResult": tool_result,
"metadata": {
"toolName": message.name.as_ref()
.or_else(|| tool_names.get(&id))
}
}));
}
if content.is_empty() {
continue;
}
let metadata = message
.metadata
.get("goose_metadata")
.and_then(|value| serde_json::from_str::<Value>(value).ok())
.unwrap_or_else(|| {
serde_json::json!({
"userVisible": true,
"agentVisible": true
})
});
let mut native = serde_json::json!({
"id": message.metadata.get("goose_message_id")
.cloned().unwrap_or_else(|| format!("supercode-goose-{index}")),
"role": role,
"created": created,
"content": content,
"metadata": metadata,
});
set_grok_target_message_extension(&mut native, message);
out.push(native);
}
out
}
fn opencode_records_from_raw(&self) -> (Option<Value>, Vec<(Value, Vec<Value>)>) {
let mut session_info: Option<Value> = None;
let mut msg_order: Vec<String> = Vec::new();
let mut msg_values: HashMap<String, Value> = HashMap::new();
let mut msg_parts: HashMap<String, Vec<Value>> = HashMap::new();
for line in &self.raw {
let Ok(env) = serde_json::from_str::<Value>(line) else {
continue;
};
let Some(key) = env.get("key").and_then(Value::as_array) else {
continue;
};
let value = env.get("value").cloned().unwrap_or(Value::Null);
match key.first().and_then(Value::as_str) {
Some("session") => session_info = Some(value),
Some("message") => {
if let Some(id) = value.get("id").and_then(Value::as_str) {
if !msg_values.contains_key(id) {
msg_order.push(id.to_string());
}
msg_values.insert(id.to_string(), value);
}
}
Some("part") => {
if let Some(mid) = value.get("messageID").and_then(Value::as_str) {
msg_parts.entry(mid.to_string()).or_default().push(value);
}
}
_ => {}
}
}
let mut ordered: Vec<(String, i64)> = msg_order
.iter()
.map(|id| {
let tc = msg_values
.get(id)
.and_then(|v| v.get("time"))
.and_then(|t| t.get("created"))
.and_then(Value::as_i64)
.unwrap_or(0);
(id.clone(), tc)
})
.collect();
ordered.sort_by(|a, b| a.1.cmp(&b.1).then_with(|| a.0.cmp(&b.0)));
let mut out = Vec::new();
for (id, _) in ordered {
let mut parts = msg_parts.remove(&id).unwrap_or_default();
parts.sort_by(|a, b| {
let ai = a.get("id").and_then(Value::as_str).unwrap_or("");
let bi = b.get("id").and_then(Value::as_str).unwrap_or("");
ai.cmp(bi)
});
if let Some(v) = msg_values.remove(&id) {
out.push((v, parts));
}
}
(session_info, out)
}
fn synthesized_opencode_info(&self) -> Value {
let id = self
.meta
.session_id
.clone()
.unwrap_or_else(|| "ses_supercode00000000000001".to_string());
let mut info = serde_json::json!({
"id": id,
"projectID": self.meta.lineage.get("projectID").cloned().unwrap_or_else(|| "global".to_string()),
"slug": self.meta.lineage.get("slug").cloned().unwrap_or_else(|| "supercode-export".to_string()),
"directory": self.cwd_string(),
"title": "supercode export",
"version": env!("CARGO_PKG_VERSION"),
"time": {"created": SYNTH_TS_MS, "updated": SYNTH_TS_MS},
});
if let Some(agent) = &self.meta.agent_id {
info["agent"] = Value::String(agent.clone());
}
if let Some(model) = &self.meta.model {
if let Some((provider, mid)) = model.split_once('/') {
info["model"] = serde_json::json!({"providerID": provider, "id": mid});
}
}
if let Some(parent) = self.meta.lineage.get("parent_session_id") {
info["parentID"] = Value::String(parent.clone());
}
if let Some(raw) = self.meta.lineage.get("claude_fork_context_ref_raw") {
info["claude_fork_context_ref"] =
serde_json::from_str(raw).unwrap_or_else(|_| Value::String(raw.clone()));
}
if let Some(extension) = codex_provenance_envelope(&self.meta) {
info[SUPERCODE_CODEX_PROVENANCE_KEY] = extension;
}
info
}
fn touch_opencode_session_updated(info: &mut Value, timestamp: i64) {
if !info.get("time").is_some_and(Value::is_object) {
info["time"] = serde_json::json!({});
}
info["time"]["updated"] = serde_json::json!(timestamp);
}
fn append_synthesized_opencode_messages(
&self,
out: &mut Vec<Value>,
messages: &[ChatMessage],
session_id: &str,
counter: &mut u64,
timestamp_cursor: &mut i64,
) -> Result<()> {
let mut calls_by_id: HashMap<&str, Vec<(usize, usize)>> = HashMap::new();
let mut results_by_id: HashMap<&str, Vec<(usize, &ChatMessage)>> = HashMap::new();
for (message_index, message) in messages.iter().enumerate() {
if message.role == Role::Assistant {
for (tool_index, call) in message.tool_calls().iter().enumerate() {
calls_by_id
.entry(call.id.as_str())
.or_default()
.push((message_index, tool_index));
}
} else if message.role == Role::Tool {
if let Some(id) = &message.tool_call_id {
results_by_id
.entry(id.as_str())
.or_default()
.push((message_index, message));
}
}
}
let mut paired_results: HashMap<(usize, usize), (usize, &ChatMessage)> = HashMap::new();
for (id, calls) in calls_by_id {
let Some(results) = results_by_id.get(id) else {
continue;
};
for (call_position, result) in calls.into_iter().zip(results.iter().copied()) {
paired_results.insert(call_position, result);
}
}
let mut i = 0;
while i < messages.len() {
let msg = &messages[i];
if is_replay_excluded(msg) {
i += 1;
continue;
}
match msg.role {
Role::System => {
let content = msg.content.clone().unwrap_or_default();
if content.trim().is_empty() {
i += 1;
continue;
}
let subtype = msg
.metadata
.get("systemSubtype")
.cloned()
.unwrap_or_else(|| "local_command".to_string());
let msg_id = opencode_fresh_id("msg", counter);
let part_id = opencode_fresh_id("prt", counter);
let timestamp = opencode_message_timestamp(msg, timestamp_cursor)?;
let mut info = serde_json::json!({
"id": msg_id,
"sessionID": session_id,
"role": "user",
"time": {"created": timestamp},
});
info[OPENCODE_SUPERCODE_MESSAGE_POSITION] = serde_json::json!(i);
let parts = vec![serde_json::json!({
"id": part_id,
"sessionID": session_id,
"messageID": msg_id,
"type": "text",
"text": content,
"synthetic": true,
"metadata": {OPENCODE_CLAUDE_SYSTEM_SUBTYPE_KEY: subtype},
})];
out.push(serde_json::json!({"info": info, "parts": parts}));
i += 1;
}
Role::User => {
let msg_id = opencode_fresh_id("msg", counter);
let parts = opencode_user_parts_from_message(msg, &msg_id, session_id, counter);
let timestamp = opencode_message_timestamp(msg, timestamp_cursor)?;
let mut info = serde_json::json!({
"id": msg_id,
"sessionID": session_id,
"role": "user",
"time": {"created": timestamp},
});
info[OPENCODE_SUPERCODE_MESSAGE_POSITION] = serde_json::json!(i);
opencode_restore_agent_model_fields(
&mut info, msg, false,
);
set_grok_message_extension(&mut info, self.meta.source, msg);
out.push(serde_json::json!({
"info": info,
"parts": parts,
}));
i += 1;
}
Role::Assistant => {
let msg_id = opencode_fresh_id("msg", counter);
let timestamp = opencode_message_timestamp(msg, timestamp_cursor)?;
let mut parts = Vec::new();
if let Some(thinking) = msg.metadata.get("thinking") {
let mut part = serde_json::json!({
"id": opencode_fresh_id("prt", counter),
"sessionID": session_id,
"messageID": msg_id,
"type": "reasoning",
"text": thinking,
"time": {"start": timestamp, "end": timestamp},
});
if let Some(signature) = msg.metadata.get("thinking_signature") {
part["metadata"] = serde_json::json!({
"anthropic": {"signature": signature},
});
}
parts.push(part);
}
if let Some(t) = &msg.content {
if !t.is_empty() {
parts.push(serde_json::json!({
"id": opencode_fresh_id("prt", counter),
"sessionID": session_id,
"messageID": msg_id,
"type": "text",
"text": t,
}));
}
}
for (tool_index, tc) in msg.tool_calls().iter().enumerate() {
let input = tc
.function
.parsed_arguments()
.unwrap_or_else(|_| Value::Object(Default::default()));
let paired_result = paired_results.get(&(i, tool_index)).copied();
let state = match paired_result {
Some((_, result)) if crate::is_tool_error(result) => {
let result_timestamp =
opencode_message_timestamp(result, timestamp_cursor)?;
serde_json::json!({
"status": "error",
"input": input,
"error": result.content.clone().unwrap_or_default(),
"time": {"end": result_timestamp},
})
}
Some((_, result)) => {
let result_timestamp =
opencode_message_timestamp(result, timestamp_cursor)?;
let mut s = serde_json::json!({
"status": "completed",
"input": input,
"output": result.content.clone().unwrap_or_default(),
"title": tc.function.name,
"time": {"end": result_timestamp},
});
if let Some(cps) = &result.content_parts {
let atts: Vec<Value> = cps
.iter()
.filter(|p| {
p.get("type").and_then(Value::as_str)
== Some("image_url")
})
.filter_map(|p| {
let url = p
.get("image_url")
.and_then(|u| u.get("url"))
.and_then(Value::as_str)?;
let mime = url
.strip_prefix("data:")
.and_then(|r| r.split_once(','))
.map(|(m, _)| m.trim_end_matches(";base64"))
.unwrap_or("application/octet-stream");
Some(serde_json::json!({
"mime": mime,
"url": url,
}))
})
.collect();
if !atts.is_empty() {
s["attachments"] = Value::Array(atts);
}
}
s
}
None => serde_json::json!({"status": "pending", "input": input}),
};
let mut part = serde_json::json!({
"id": opencode_fresh_id("prt", counter),
"sessionID": session_id,
"messageID": msg_id,
"type": "tool",
"callID": tc.id,
"tool": tc.function.name,
"state": state,
});
if let Some((result_position, _)) = paired_result {
part[OPENCODE_SUPERCODE_RESULT_POSITION] =
serde_json::json!(result_position);
}
if paired_result.is_some_and(|(_, result)| {
crate::tool_outcome(result) == crate::ToolOutcome::Unknown
}) {
part[SUPERCODE_TOOL_OUTCOME_KEY] = Value::String("unknown".to_string());
}
if let Some((_, result)) = paired_result {
set_grok_message_extension(&mut part, self.meta.source, result);
}
parts.push(part);
}
let mut info = serde_json::json!({
"id": msg_id,
"sessionID": session_id,
"role": "assistant",
"time": {"created": timestamp},
});
info[OPENCODE_SUPERCODE_MESSAGE_POSITION] = serde_json::json!(i);
opencode_restore_agent_model_fields(
&mut info, msg, true,
);
set_grok_message_extension(&mut info, self.meta.source, msg);
out.push(serde_json::json!({
"info": info,
"parts": parts,
}));
i += 1;
}
Role::Tool => i += 1,
}
}
Ok(())
}
fn to_opencode_jsonl(&self) -> Result<String> {
let mut info = self.synthesized_opencode_info();
let ses_id = info
.get("id")
.and_then(Value::as_str)
.unwrap_or("ses_new")
.to_string();
let mut messages_json: Vec<Value> = Vec::new();
let mut counter: u64 = 0;
let mut timestamp_cursor =
opencode_max_timestamp(&info).unwrap_or_else(|| SYNTH_TS_MS.saturating_sub(1));
self.append_synthesized_opencode_messages(
&mut messages_json,
&self.messages,
&ses_id,
&mut counter,
&mut timestamp_cursor,
)?;
if !messages_json.is_empty() {
Self::touch_opencode_session_updated(&mut info, timestamp_cursor);
}
let doc = serde_json::json!({"info": info, "messages": messages_json});
Ok(serde_json::to_string_pretty(&doc).unwrap_or_default())
}
fn to_opencode_jsonl_spliced(&self, session_id: Option<&str>) -> Result<String> {
if self.raw.is_empty() {
return self.to_opencode_jsonl();
}
let (session_info, records) = self.opencode_records_from_raw();
let (_, message_prefix_len) = self.spliced_prefix_lens();
let mut info = session_info.unwrap_or_else(|| self.synthesized_opencode_info());
if let Some(id) = session_id {
info["id"] = Value::String(id.to_string());
}
let ses_id_for_new = info
.get("id")
.and_then(Value::as_str)
.unwrap_or("ses_new")
.to_string();
let mut timestamp_cursor = std::iter::once(opencode_max_timestamp(&info))
.chain(records.iter().flat_map(|(msg, parts)| {
std::iter::once(opencode_max_timestamp(msg))
.chain(parts.iter().map(opencode_max_timestamp))
}))
.flatten()
.max()
.unwrap_or_else(|| SYNTH_TS_MS.saturating_sub(1));
let mut messages_json: Vec<Value> = records
.into_iter()
.map(|(msg, parts)| serde_json::json!({"info": msg, "parts": parts}))
.collect();
let imported_len = messages_json.len();
let mut counter: u64 = 0;
self.append_synthesized_opencode_messages(
&mut messages_json,
&self.messages[message_prefix_len..],
&ses_id_for_new,
&mut counter,
&mut timestamp_cursor,
)?;
if messages_json.len() > imported_len {
Self::touch_opencode_session_updated(&mut info, timestamp_cursor);
}
let doc = serde_json::json!({"info": info, "messages": messages_json});
Ok(serde_json::to_string_pretty(&doc).unwrap_or_default())
}
pub fn to_opencode_direct_write(&self, data_root: &Path) -> Result<PathBuf> {
let (session_info, mut records) = self.opencode_records_from_raw();
let mut info = session_info.unwrap_or_else(|| self.synthesized_opencode_info());
let ses_id = info
.get("id")
.and_then(Value::as_str)
.unwrap_or("ses_new")
.to_string();
if info.get("id").is_none() {
info["id"] = Value::String(ses_id.clone());
}
let project_id = info
.get("projectID")
.and_then(Value::as_str)
.unwrap_or("global")
.to_string();
let (_, message_prefix_len) = self.spliced_prefix_lens();
let mut counter: u64 = 0;
let mut timestamp_cursor = std::iter::once(opencode_max_timestamp(&info))
.chain(records.iter().flat_map(|(msg, parts)| {
std::iter::once(opencode_max_timestamp(msg))
.chain(parts.iter().map(opencode_max_timestamp))
}))
.flatten()
.max()
.unwrap_or_else(|| SYNTH_TS_MS.saturating_sub(1));
let mut appended_json: Vec<Value> = Vec::new();
self.append_synthesized_opencode_messages(
&mut appended_json,
&self.messages[message_prefix_len..],
&ses_id,
&mut counter,
&mut timestamp_cursor,
)?;
if !appended_json.is_empty() {
Self::touch_opencode_session_updated(&mut info, timestamp_cursor);
}
for entry in appended_json {
let msg = entry.get("info").cloned().unwrap_or(Value::Null);
let parts = entry
.get("parts")
.and_then(Value::as_array)
.cloned()
.unwrap_or_default();
records.push((msg, parts));
}
let storage = data_root.join("storage");
let session_dir = storage.join("session").join(&project_id);
std::fs::create_dir_all(&session_dir)?;
std::fs::write(
session_dir.join(format!("{ses_id}.json")),
serde_json::to_string_pretty(&info).unwrap_or_default(),
)?;
let message_dir = storage.join("message").join(&ses_id);
let part_dir = storage.join("part");
std::fs::create_dir_all(&message_dir)?;
for (msg, parts) in &records {
let Some(msg_id) = msg.get("id").and_then(Value::as_str) else {
continue;
};
std::fs::write(
message_dir.join(format!("{msg_id}.json")),
serde_json::to_string_pretty(msg).unwrap_or_default(),
)?;
let this_part_dir = part_dir.join(msg_id);
std::fs::create_dir_all(&this_part_dir)?;
for part in parts {
let Some(part_id) = part.get("id").and_then(Value::as_str) else {
continue;
};
std::fs::write(
this_part_dir.join(format!("{part_id}.json")),
serde_json::to_string_pretty(part).unwrap_or_default(),
)?;
}
}
for header in &self.meta.opencode_headers {
let Some(key) = header.get("key").and_then(Value::as_array) else {
continue;
};
let Some(kind) = key.first().and_then(Value::as_str) else {
continue;
};
let value = header.get("value").cloned().unwrap_or(Value::Null);
if !matches!(kind, "session_diff" | "todo") {
continue;
}
let dir = storage.join(kind);
std::fs::create_dir_all(&dir)?;
std::fs::write(
dir.join(format!("{ses_id}.json")),
serde_json::to_string_pretty(&value).unwrap_or_default(),
)?;
}
Ok(session_dir)
}
}
fn opencode_fresh_id(prefix: &str, counter: &mut u64) -> String {
*counter += 1;
format!("{prefix}_synth{counter:06}")
}
fn opencode_restore_agent_model_fields(info: &mut Value, msg: &ChatMessage, is_assistant: bool) {
if let Some(agent) = msg.metadata.get("agent") {
info["agent"] = Value::String(agent.clone());
}
if let Some(model) = msg.metadata.get("model") {
if is_assistant {
if let Some((provider, model_id)) = model.split_once('/') {
info["providerID"] = Value::String(provider.to_string());
info["modelID"] = Value::String(model_id.to_string());
}
} else if let Ok(v) = serde_json::from_str::<Value>(model) {
info["model"] = v;
}
}
if !is_assistant {
return;
}
if msg.metadata.get("is_summary").map(String::as_str) == Some("true") {
info["summary"] = Value::Bool(true);
}
if let Some(finish) = msg.metadata.get("finish") {
info["finish"] = Value::String(finish.clone());
}
if let Some(cost) = msg.metadata.get("cost") {
if let Ok(v) = serde_json::from_str::<Value>(cost) {
info["cost"] = v;
}
}
if let Some(tokens) = msg.metadata.get("tokens") {
if let Ok(v) = serde_json::from_str::<Value>(tokens) {
info["tokens"] = v;
}
}
}
fn opencode_user_parts_from_message(
msg: &ChatMessage,
msg_id: &str,
session_id: &str,
counter: &mut u64,
) -> Vec<Value> {
let mut parts = Vec::new();
if let Some(cps) = &msg.content_parts {
for p in cps {
match p.get("type").and_then(Value::as_str) {
Some("text") => {
if let Some(t) = p.get("text").and_then(Value::as_str) {
parts.push(serde_json::json!({
"id": opencode_fresh_id("prt", counter),
"sessionID": session_id,
"messageID": msg_id,
"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)
{
let mime = url
.strip_prefix("data:")
.and_then(|r| r.split_once(','))
.map(|(m, _)| m.trim_end_matches(";base64"))
.unwrap_or("application/octet-stream");
parts.push(serde_json::json!({
"id": opencode_fresh_id("prt", counter),
"sessionID": session_id,
"messageID": msg_id,
"type": "file",
"mime": mime,
"url": url,
}));
}
}
_ => {}
}
}
} else if let Some(t) = &msg.content {
if !t.is_empty() {
parts.push(serde_json::json!({
"id": opencode_fresh_id("prt", counter),
"sessionID": session_id,
"messageID": msg_id,
"type": "text",
"text": t,
}));
}
}
parts
}
fn codex_response_item(payload: Value, ts: &str) -> Value {
serde_json::json!({"timestamp": ts, "type": "response_item", "payload": payload})
}
fn with_turn_id(mut payload: Value, turn_id: Option<&str>) -> Value {
if let Some(tid) = turn_id {
payload["metadata"] = serde_json::json!({"turn_id": tid});
}
payload
}
fn codex_message_content_blocks(text_type: &str, 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_type, "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(serde_json::json!({
"type": "input_image",
"image_url": url,
}));
}
}
_ => {}
}
}
Value::Array(blocks)
}
None => {
let text = msg.content.clone().unwrap_or_default();
Value::Array(vec![serde_json::json!({"type": text_type, "text": text})])
}
}
}
fn codex_tool_output_text(msg: &ChatMessage) -> String {
let mut text = msg.content.clone().unwrap_or_default();
if let Some(parts) = &msg.content_parts {
let n = parts
.iter()
.filter(|p| p.get("type").and_then(Value::as_str) == Some("image_url"))
.count();
if n > 0 {
if !text.is_empty() {
text.push('\n');
}
text.push_str(&format!(
"[image: {n} nested image(s) dropped — codex tool output has no \
structured content slot to carry them]"
));
}
}
text
}
fn push_pi_header(
out: &mut String,
id: &str,
cwd: &str,
parent_session: Option<&str>,
created_at: Option<&str>,
claude_fork_context_ref: Option<&str>,
) {
let mut header = serde_json::json!({
"type": "session",
"version": 3,
"id": id,
"timestamp": created_at.unwrap_or(SYNTH_TS),
"cwd": cwd,
});
if let Some(ps) = parent_session {
header["parentSession"] = Value::String(ps.to_string());
}
if let Some(raw) = claude_fork_context_ref {
header["claude_fork_context_ref"] =
serde_json::from_str(raw).unwrap_or_else(|_| Value::String(raw.to_string()));
}
push_jsonl(out, &header);
}
fn pi_fresh_id(used: &mut HashSet<String>, counter: &mut u64) -> String {
loop {
*counter += 1;
let h = counter.wrapping_mul(0x9E3779B97F4A7C15);
let id = format!("{:08x}", (h >> 32) as u32);
if used.insert(id.clone()) {
return id;
}
}
}
fn parse_data_uri(url: &str) -> Option<(String, String)> {
let rest = url.strip_prefix("data:")?;
let (meta, data) = rest.split_once(',')?;
let mime = meta.strip_suffix(";base64").unwrap_or(meta);
Some((mime.to_string(), data.to_string()))
}
fn pi_content_value(msg: &ChatMessage) -> Value {
if let Some(parts) = &msg.content_parts {
let mut arr = 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) {
arr.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)
{
if let Some((mime, data)) = parse_data_uri(url) {
arr.push(
serde_json::json!({"type": "image", "mimeType": mime, "data": data}),
);
}
}
}
_ => {}
}
}
Value::Array(arr)
} else {
Value::String(msg.content.clone().unwrap_or_default())
}
}
fn pi_assistant_content_value(msg: &ChatMessage) -> Value {
let mut arr = Vec::new();
if let Some(thinking) = msg.metadata.get("thinking") {
let mut block = serde_json::json!({"type": "thinking", "thinking": thinking});
if let Some(sig) = msg.metadata.get("thinking_signature") {
block["thinkingSignature"] = Value::String(sig.clone());
}
if msg.metadata.get("pi_thinking_redacted").map(String::as_str) == Some("true") {
block["redacted"] = Value::Bool(true);
}
arr.push(block);
}
if let Some(text) = &msg.content {
if !text.is_empty() {
let mut block = serde_json::json!({"type": "text", "text": text});
if let Some(sig) = msg.metadata.get("pi_text_signature") {
block["textSignature"] = Value::String(sig.clone());
}
arr.push(block);
}
}
for tc in msg.tool_calls() {
let args = tc
.function
.parsed_arguments()
.unwrap_or_else(|_| Value::Object(Default::default()));
let mut block = serde_json::json!({
"type": "toolCall",
"id": tc.id,
"name": tc.function.name,
"arguments": args,
});
if let Some(sig) = msg.metadata.get("pi_thought_signature") {
block["thoughtSignature"] = Value::String(sig.clone());
}
arr.push(block);
}
Value::Array(arr)
}
fn default_pi_usage() -> Value {
serde_json::json!({
"input": 0, "output": 0, "cacheRead": 0, "cacheWrite": 0, "totalTokens": 0,
"cost": {"input": 0.0, "output": 0.0, "cacheRead": 0.0, "cacheWrite": 0.0, "total": 0.0},
})
}
fn is_tool_error_flag(msg: &ChatMessage) -> bool {
msg.metadata.get("pi_is_error").map(String::as_str) == Some("true") || crate::is_tool_error(msg)
}
#[cfg(test)]
mod tests {
use super::{
opencode_message_timestamp, parent_tool_use_index, truncate_messages_with_anchor, Session,
SessionFormat,
};
use crate::message::ChatMessage;
use crate::{Fidelity, Role};
#[test]
fn a_new_user_message_slides_the_display_tail_without_becoming_its_only_anchor() {
let mut messages = vec![
ChatMessage::user("original prompt"),
ChatMessage::assistant("one"),
ChatMessage::assistant("two"),
ChatMessage::assistant("three"),
ChatMessage::assistant("four"),
ChatMessage::assistant("five"),
ChatMessage::user("new prompt"),
];
truncate_messages_with_anchor(&mut messages, 4, Vec::new());
assert_eq!(messages.len(), 4);
assert_eq!(messages[0].content.as_deref(), Some("original prompt"));
assert_eq!(messages[3].content.as_deref(), Some("new prompt"));
}
#[test]
fn a_tool_heavy_current_turn_keeps_the_previous_and_current_user_anchors() {
let mut messages = vec![
ChatMessage::user("previous prompt"),
ChatMessage::assistant("previous answer"),
ChatMessage::user("current prompt"),
ChatMessage::assistant("tool one"),
ChatMessage::assistant("tool two"),
ChatMessage::assistant("tool three"),
ChatMessage::assistant("tool four"),
ChatMessage::assistant("tool five"),
];
truncate_messages_with_anchor(&mut messages, 4, Vec::new());
assert_eq!(messages.len(), 4);
assert_eq!(messages[0].content.as_deref(), Some("previous prompt"));
assert_eq!(messages[1].content.as_deref(), Some("current prompt"));
assert_eq!(messages[3].content.as_deref(), Some("tool five"));
}
#[test]
fn a_preceding_user_anchor_is_restored_when_dedup_shrinks_below_the_limit() {
let mut messages = vec![
ChatMessage::user("current prompt"),
ChatMessage::assistant("tool one"),
ChatMessage::assistant("tool two"),
];
truncate_messages_with_anchor(&mut messages, 4, vec![ChatMessage::user("previous prompt")]);
assert_eq!(messages.len(), 4);
assert_eq!(messages[0].content.as_deref(), Some("previous prompt"));
assert_eq!(messages[1].content.as_deref(), Some("current prompt"));
}
#[test]
fn a_tool_heavy_turn_restores_two_users_that_both_left_the_retention_buffer() {
let mut messages = vec![
ChatMessage::assistant("tool one"),
ChatMessage::assistant("tool two"),
ChatMessage::assistant("tool three"),
ChatMessage::assistant("tool four"),
];
truncate_messages_with_anchor(
&mut messages,
4,
vec![
ChatMessage::user("previous prompt"),
ChatMessage::user("current prompt"),
],
);
assert_eq!(messages.len(), 4);
assert_eq!(messages[0].content.as_deref(), Some("previous prompt"));
assert_eq!(messages[1].content.as_deref(), Some("current prompt"));
assert_eq!(messages[3].content.as_deref(), Some("tool four"));
}
#[test]
fn bounded_codex_file_expands_past_tool_noise_and_loads_real_earlier_history() {
let nonce = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos();
let path = std::env::temp_dir().join(format!(
"supercode-display-history-{}-{nonce}.jsonl",
std::process::id()
));
let user = |text: &str| {
format!(
r#"{{"type":"response_item","payload":{{"type":"message","role":"user","content":[{{"type":"input_text","text":"{text}"}}]}}}}"#,
)
};
let assistant = |index: usize| {
format!(
r#"{{"type":"response_item","payload":{{"type":"message","role":"assistant","content":[{{"type":"output_text","text":"answer-{index}"}}]}}}}"#,
)
};
let mut lines = vec![
r#"{"type":"session_meta","payload":{"id":"session-1"}}"#.to_string(),
user("earlier prompt"),
format!(
r#"{{"type":"event_msg","payload":{{"type":"token_count","noise":"{}"}}}}"#,
"x".repeat(5 * 1024 * 1024)
),
user("latest prompt"),
];
lines.extend((0..130).map(assistant));
std::fs::write(&path, format!("{}\n", lines.join("\n"))).unwrap();
let initial = Session::load_display_view(&path, Fidelity::Semantic, 120).unwrap();
let expanded = Session::load_display_view(&path, Fidelity::Semantic, 240).unwrap();
std::fs::remove_file(&path).unwrap();
let initial_users = initial
.messages
.iter()
.filter(|message| message.role == Role::User)
.filter_map(|message| message.content.as_deref())
.collect::<Vec<_>>();
assert_eq!(initial.messages.len(), 120);
assert_eq!(initial_users, ["earlier prompt", "latest prompt"]);
assert!(
initial.imported_message_count.unwrap() > initial.messages.len(),
"a bounded initial page must truthfully report earlier history"
);
assert_eq!(expanded.messages.len(), 132);
assert_eq!(expanded.imported_message_count, Some(132));
}
#[test]
fn bounded_codex_display_history_reports_the_unbounded_message_total() {
let jsonl = (0..6)
.map(|index| {
let role = if index % 2 == 0 { "user" } else { "assistant" };
format!(
r#"{{"type":"response_item","payload":{{"type":"message","role":"{role}","content":[{{"type":"input_text","text":"message-{index}"}}]}}}}"#,
)
})
.collect::<Vec<_>>()
.join("\n");
let session = Session::from_codex_display_str(&jsonl, 2).unwrap();
assert_eq!(session.messages.len(), 2);
assert_eq!(session.imported_message_count, Some(6));
}
#[test]
fn malformed_discriminated_native_turn_is_counted_as_parse_loss() {
let base = Session::from_native_messages(Vec::new());
let mut native = base.to_native_jsonl_v2(&[]);
native.push_str("{\"supercode_turn\":1}\n");
let parsed = Session::from_native_str(&native).unwrap();
assert_eq!(parsed.parse_error_lines, 1);
assert!(parsed.messages.is_empty());
assert_eq!(
parsed.raw.last().map(String::as_str),
Some("{\"supercode_turn\":1}")
);
}
#[test]
fn spliced_export_refuses_a_native_wrapper_with_parse_loss() {
let imported = Session::from_claude_code_str(
r#"{"type":"user","sessionId":"s","cwd":"/tmp","message":{"role":"user","content":"hi"}}"#,
)
.unwrap();
let mut native = imported.to_native_jsonl_v2(&[ChatMessage::assistant("continued")]);
native.push_str("{\"supercode_turn\":1}\n");
let parsed = Session::from_native_str(&native).unwrap();
let error = parsed
.to_jsonl_spliced(SessionFormat::ClaudeCode, None)
.unwrap_err();
assert!(error.to_string().contains("parse loss"), "{error}");
}
#[test]
fn sidecar_loader_requires_a_supported_native_header() {
for malformed in [
"",
"not-json\n",
"{}\n",
"{\"supercode_native\":2}\n",
"{\"supercode_native\":99,\"source\":\"native\"}\n",
] {
let error = Session::from_sidecar_str(malformed).unwrap_err();
assert!(error.to_string().contains("sidecar header"), "{error}");
}
}
#[test]
fn gemini_user_parts_preserve_text_media_and_response_order() {
let session = Session::from_gemini_str(
r#"{"sessionId":"11111111-1111-4111-8111-111111111111","projectHash":"p"}
{"type":"gemini","content":"calling","toolCalls":[{"id":"a","name":"read","args":{}},{"id":"b","name":"read","args":{}}]}
{"type":"user","content":[{"text":"before"},{"functionResponse":{"id":"b","name":"read","response":{"output":"B"}}},{"inlineData":{"mimeType":"image/png","data":"YQ=="}},{"functionResponse":{"name":"read","response":{"output":"A"}}},{"text":"after"}]}
"#,
)
.unwrap();
assert_eq!(session.messages.len(), 6);
assert_eq!(
session.messages[1].content_parts.as_ref().unwrap()[0]["text"],
"before"
);
assert_eq!(session.messages[2].tool_call_id.as_deref(), Some("b"));
assert!(
session.messages[3].content_parts.as_ref().unwrap()[0]["image_url"]["url"]
.as_str()
.unwrap()
.starts_with("data:image/png;base64,")
);
assert_eq!(session.messages[4].tool_call_id.as_deref(), Some("a"));
assert_eq!(
session.messages[5].content_parts.as_ref().unwrap()[0]["text"],
"after"
);
}
#[test]
fn opencode_synthetic_timestamp_fails_closed_at_i64_max() {
let msg = ChatMessage::user("continuation");
let mut cursor = i64::MAX - 1;
assert_eq!(
opencode_message_timestamp(&msg, &mut cursor).unwrap(),
i64::MAX
);
let err = opencode_message_timestamp(&msg, &mut cursor).unwrap_err();
assert!(err.to_string().contains("after i64::MAX"));
assert_eq!(cursor, i64::MAX, "overflow must not wrap or mutate state");
}
#[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());
}
}