1use super::*;
4
5impl Session {
6 pub fn from_grok(path: impl AsRef<Path>) -> Result<Session> {
13 let path = path.as_ref();
14 let mut session = Self::from_grok_str(&std::fs::read_to_string(path)?)?;
15 session.capture_grok_path_metadata(path);
16 Ok(session)
17 }
18
19 pub fn from_grok_str(jsonl: &str) -> Result<Session> {
27 let mut meta = SessionMeta::new(SessionSource::Grok);
28 let (raw_lines, raw_trailing_newline) = split_lines_verbatim(jsonl);
29 let raw: Vec<String> = raw_lines.iter().map(|line| line.to_string()).collect();
30 let mut messages = Vec::new();
31 let mut parse_error_lines = 0usize;
32 let mut tool_names: HashMap<String, String> = HashMap::new();
33
34 for (record_index, line) in non_empty_lines(jsonl).enumerate() {
35 let value: Value = match serde_json::from_str(line) {
36 Ok(value) => value,
37 Err(_) => {
38 parse_error_lines += 1;
39 continue;
40 }
41 };
42 restore_codex_provenance_from_top_level(&value, &mut meta)?;
43 if let Some(kind) = grok_residue_kind(&value) {
46 capture_native_residue(&mut meta, "grok", record_index, line, &value, &kind);
47 }
48 match value.get("type").and_then(Value::as_str) {
49 Some("system") => {
50 if meta.system_prompt.is_none() {
51 meta.system_prompt = value
52 .get("content")
53 .and_then(Value::as_str)
54 .map(str::to_string);
55 }
56 }
57 Some("user") => {
58 let content = extract_text_content(value.get("content"));
59 let role = if value.get("synthetic_reason").and_then(Value::as_str)
60 == Some("supercode_system_event")
61 {
62 Role::System
63 } else {
64 Role::User
65 };
66 let content = if role == Role::User {
67 match grok_human_user_text(&content) {
68 Some(content) => content,
69 None if value.get(SUPERCODE_GROK_MESSAGE_KEY).is_some() => {
70 String::new()
71 }
72 None => continue,
73 }
74 } else {
75 content
76 };
77 let mut message = ChatMessage {
78 role,
79 content: Some(content),
80 content_parts: None,
81 tool_calls: None,
82 tool_call_id: None,
83 name: None,
84 metadata: Default::default(),
85 };
86 capture_grok_scalar_metadata(
87 &value,
88 &mut message,
89 &["prompt_index", "prior_turn_interrupt", "synthetic_reason"],
90 );
91 restore_grok_message_extension(&value, &mut message);
92 messages.push(message);
93 }
94 Some("assistant") => {
95 let calls: Vec<ToolCall> = value
96 .get("tool_calls")
97 .and_then(Value::as_array)
98 .into_iter()
99 .flatten()
100 .filter_map(|call| {
101 let id = call.get("id")?.as_str()?.to_string();
102 let name = call.get("name")?.as_str()?.to_string();
103 let arguments = call
104 .get("arguments")
105 .map(value_to_arg_string)
106 .unwrap_or_else(|| "{}".to_string());
107 tool_names.insert(id.clone(), name.clone());
108 Some(function_call(&id, &name, arguments))
109 })
110 .collect();
111 let content = value
112 .get("content")
113 .and_then(Value::as_str)
114 .filter(|content| !content.is_empty())
115 .map(str::to_string);
116 let mut message = ChatMessage {
117 role: Role::Assistant,
118 content,
119 content_parts: None,
120 tool_calls: (!calls.is_empty()).then_some(calls),
121 tool_call_id: None,
122 name: None,
123 metadata: Default::default(),
124 };
125 capture_grok_scalar_metadata(
126 &value,
127 &mut message,
128 &["model_id", "model_fingerprint", "reasoning_effort"],
129 );
130 if let Some(model) = value.get("model_id").and_then(Value::as_str) {
131 meta.model = Some(model.to_string());
132 }
133 restore_grok_message_extension(&value, &mut message);
134 messages.push(message);
135 }
136 Some("tool_result") => {
137 let id = value
138 .get("tool_call_id")
139 .and_then(Value::as_str)
140 .unwrap_or_default();
141 let content = value
142 .get("content")
143 .map(|value| match value {
144 Value::String(text) => text.clone(),
145 other => extract_text_content(Some(other)),
146 })
147 .unwrap_or_default();
148 let mut message = tool_message(id, content);
149 message.name = tool_names.get(id).cloned();
150 let images: Vec<Value> = value
152 .get("images")
153 .and_then(Value::as_array)
154 .into_iter()
155 .flatten()
156 .filter_map(|image| image.get("url").and_then(Value::as_str))
157 .filter(|url| !url.is_empty())
158 .map(|url| serde_json::json!({"type": "image_url", "image_url": {"url": url}}))
159 .collect();
160 if !images.is_empty() {
161 let mut parts = Vec::new();
162 if let Some(text) = message.content.as_deref().filter(|t| !t.is_empty()) {
163 parts.push(serde_json::json!({"type": "text", "text": text}));
164 }
165 parts.extend(images);
166 message.content_parts = Some(parts);
167 }
168 restore_grok_message_extension(&value, &mut message);
169 messages.push(message);
170 }
171 _ => {}
175 }
176 }
177
178 ensure_tool_results_paired(&mut messages);
179 let imported_message_count = Some(messages.len());
180 Ok(Session {
181 meta,
182 messages,
183 subagents: Vec::new(),
184 raw,
185 raw_trailing_newline,
186 imported_message_count,
187 raw_is_verbatim: true,
188 parse_error_lines,
189 load_residue: Vec::new(),
190 })
191 }
192
193 pub(super) fn capture_grok_path_metadata(&mut self, transcript: &Path) {
194 let Some(session_dir) = transcript.parent() else {
195 return;
196 };
197 self.meta.session_id = session_dir
198 .file_name()
199 .and_then(|name| name.to_str())
200 .map(str::to_string);
201 self.meta.cwd = session_dir
202 .parent()
203 .and_then(Path::file_name)
204 .and_then(|name| name.to_str())
205 .and_then(percent_decode_path)
206 .map(PathBuf::from);
207
208 let Ok(summary_text) = std::fs::read_to_string(session_dir.join("summary.json")) else {
209 return;
210 };
211 let Ok(summary) = serde_json::from_str::<Value>(&summary_text) else {
212 return;
213 };
214 if let Some(model) = summary.get("current_model_id").and_then(Value::as_str) {
215 self.meta.model = Some(model.to_string());
216 }
217 for (source, target) in [
218 ("generated_title", "session_name"),
219 ("created_at", "created_at"),
220 ("updated_at", "updated_at"),
221 ("chat_format_version", "grok_chat_format_version"),
222 ] {
223 if let Some(value) = summary.get(source) {
224 self.meta.lineage.insert(
225 target.to_string(),
226 value
227 .as_str()
228 .map(str::to_string)
229 .unwrap_or_else(|| value.to_string()),
230 );
231 }
232 }
233 }
234}
235
236const GROK_CONSUMED_TYPES: [&str; 4] = ["system", "user", "assistant", "tool_result"];
240
241pub(super) fn grok_residue_kind(record: &Value) -> Option<String> {
242 record.as_object()?;
243 match record.get("type").and_then(Value::as_str) {
244 Some(kind) if !GROK_CONSUMED_TYPES.contains(&kind) => Some(kind.to_string()),
245 Some(_) => None,
246 None => Some("untyped".to_string()),
247 }
248}
249
250fn capture_grok_scalar_metadata(value: &Value, message: &mut ChatMessage, keys: &[&str]) {
253 for key in keys {
254 if let Some(value) = value.get(*key) {
255 message.metadata.insert(
256 format!("grok_{key}"),
257 value
258 .as_str()
259 .map(str::to_string)
260 .unwrap_or_else(|| value.to_string()),
261 );
262 }
263 }
264}
265
266fn grok_human_user_text(raw: &str) -> Option<String> {
267 let text = raw.trim();
268 if text.is_empty() || text.starts_with("<user_info>") || text.starts_with("<system-reminder>") {
269 return None;
270 }
271 let unwrapped = text
272 .strip_prefix("<user_query>")
273 .and_then(|value| value.strip_suffix("</user_query>"))
274 .map(str::trim)
275 .unwrap_or(text);
276 (!unwrapped.is_empty()).then(|| unwrapped.to_string())
277}
278
279const SUPERCODE_GROK_MESSAGE_KEY: &str = "_supercode_grok_message";
287
288fn grok_message_extension(source: SessionSource, message: &ChatMessage) -> Option<Value> {
289 let metadata = message
290 .metadata
291 .iter()
292 .filter(|(key, _)| {
293 key.starts_with("gemini_") || key.starts_with("grok_") || key.starts_with("goose_")
294 })
295 .map(|(key, value)| (key.clone(), Value::String(value.clone())))
296 .collect::<serde_json::Map<_, _>>();
297
298 let has_portable_fields =
303 !metadata.is_empty() || message.metadata.contains_key("codex_custom_tool_call_ids");
304 (matches!(
305 source,
306 SessionSource::Gemini | SessionSource::Grok | SessionSource::Goose
307 ) || has_portable_fields
308 || message.content_parts.is_some())
309 .then(|| {
310 serde_json::json!({
311 "schema": 2,
312 "role": message.role,
313 "content": message.content,
314 "content_parts": message.content_parts,
315 "tool_calls": message.tool_calls,
316 "tool_call_id": message.tool_call_id,
317 "name": message.name,
318 "metadata": message.metadata,
319 })
320 })
321}
322
323pub(super) fn set_grok_target_message_extension(value: &mut Value, message: &ChatMessage) {
324 value[SUPERCODE_GROK_MESSAGE_KEY] = serde_json::json!({
325 "schema": 2,
326 "role": message.role,
327 "content": message.content,
328 "content_parts": message.content_parts,
329 "tool_calls": message.tool_calls,
330 "tool_call_id": message.tool_call_id,
331 "name": message.name,
332 "metadata": message.metadata,
333 });
334}
335
336pub(super) fn set_grok_message_extension(
337 value: &mut Value,
338 source: SessionSource,
339 message: &ChatMessage,
340) {
341 if let Some(extension) = grok_message_extension(source, message) {
342 value[SUPERCODE_GROK_MESSAGE_KEY] = extension;
343 }
344}
345
346pub(super) fn restore_grok_message_extension(value: &Value, message: &mut ChatMessage) {
347 let Some(extension) = value.get(SUPERCODE_GROK_MESSAGE_KEY) else {
348 return;
349 };
350 let codex_open_turn = message.metadata.get("__codex_open_turn").cloned();
355 let codex_turn_id = message.metadata.get("turn_id").cloned();
356 let extension_has_turn_id = extension
357 .get("metadata")
358 .and_then(Value::as_object)
359 .is_some_and(|metadata| metadata.contains_key("turn_id"));
360 if extension.get("schema").and_then(Value::as_u64) == Some(2) {
361 if let Some(role) = extension
362 .get("role")
363 .and_then(|value| serde_json::from_value(value.clone()).ok())
364 {
365 message.role = role;
366 }
367 message.content = extension
368 .get("content")
369 .and_then(Value::as_str)
370 .map(str::to_string);
371 message.content_parts = extension
372 .get("content_parts")
373 .and_then(|value| serde_json::from_value(value.clone()).ok());
374 message.tool_call_id = extension
380 .get("tool_call_id")
381 .and_then(Value::as_str)
382 .map(str::to_string);
383 message.name = extension
384 .get("name")
385 .and_then(Value::as_str)
386 .map(str::to_string);
387 message.metadata.clear();
388 }
389 if let Some(metadata) = extension.get("metadata").and_then(Value::as_object) {
390 for (key, value) in metadata {
391 if let Some(value) = value.as_str() {
392 message.metadata.insert(key.clone(), value.to_string());
393 }
394 }
395 }
396 if let Some(name) = extension.get("name").and_then(Value::as_str) {
397 message.name = Some(name.to_string());
398 }
399 if let Some(marker) = codex_open_turn {
400 message
401 .metadata
402 .insert("__codex_open_turn".to_string(), marker);
403 }
404 if let Some(turn_id) = codex_turn_id {
405 message.metadata.insert("turn_id".to_string(), turn_id);
406 if !extension_has_turn_id {
407 message.metadata.insert(
408 "__grok_remove_synthetic_turn_id".to_string(),
409 "true".to_string(),
410 );
411 }
412 }
413}
414
415pub(super) fn restore_single_grok_message(value: &Value, messages: &mut [ChatMessage]) {
416 if let [message] = messages {
417 restore_grok_message_extension(value, message);
418 }
419}
420
421impl Session {
422 pub(super) fn to_grok_jsonl(&self) -> String {
426 let mut out = String::new();
427 if let Some(prompt) = self
428 .meta
429 .system_prompt
430 .as_deref()
431 .filter(|prompt| !prompt.is_empty())
432 {
433 push_jsonl(
434 &mut out,
435 &serde_json::json!({
436 "type": "system",
437 "content": prompt,
438 }),
439 );
440 }
441 self.write_grok_records(&mut out, &self.messages);
442 if self.meta.native_residue_source.as_deref() == Some("grok") {
446 let mut records: Vec<&Value> = self.meta.native_residue.iter().collect();
447 records.sort_by_key(|entry| {
448 entry
449 .get("record_index")
450 .and_then(Value::as_u64)
451 .unwrap_or(u64::MAX)
452 });
453 for entry in records {
454 if let Some(raw) = entry.get("raw").and_then(Value::as_str) {
455 out.push_str(raw);
456 out.push('\n');
457 }
458 }
459 } else if let Some(extension) = native_residue_envelope(&self.meta) {
460 if out.is_empty() {
461 push_jsonl(
462 &mut out,
463 &serde_json::json!({"type": "system", "content": ""}),
464 );
465 }
466 inject_first_jsonl_top_level(
467 &mut out,
468 SUPERCODE_NATIVE_RESIDUE_SUMMARY_KEY,
469 native_residue_summary(&extension),
470 );
471 inject_first_jsonl_top_level(&mut out, SUPERCODE_NATIVE_RESIDUE_KEY, extension);
472 }
473 out
474 }
475
476 fn write_grok_records(&self, out: &mut String, messages: &[ChatMessage]) {
477 for message in messages {
478 if is_replay_excluded(message) {
479 continue;
480 }
481 let mut value = match message.role {
482 Role::System => serde_json::json!({
483 "type": "user",
484 "content": [{"type": "text", "text": message.content.clone().unwrap_or_default()}],
485 "synthetic_reason": "supercode_system_event",
486 }),
487 Role::User => {
488 let mut value = serde_json::json!({
489 "type": "user",
490 "content": [{"type": "text", "text": message.content.clone().unwrap_or_default()}],
491 });
492 if let Some(object) = value.as_object_mut() {
493 for (metadata, field) in [
494 ("grok_prompt_index", "prompt_index"),
495 ("grok_prior_turn_interrupt", "prior_turn_interrupt"),
496 ("grok_synthetic_reason", "synthetic_reason"),
497 ] {
498 if let Some(raw) = message.metadata.get(metadata) {
499 object.insert(
500 field.to_string(),
501 serde_json::from_str(raw)
502 .unwrap_or_else(|_| Value::String(raw.clone())),
503 );
504 }
505 }
506 }
507 value
508 }
509 Role::Assistant => {
510 let calls = message
511 .tool_calls()
512 .iter()
513 .map(|call| {
514 serde_json::json!({
515 "id": call.id,
516 "name": call.function.name,
517 "arguments": call.function.arguments,
518 })
519 })
520 .collect::<Vec<_>>();
521 let mut value = serde_json::json!({
522 "type": "assistant",
523 "content": message.content.clone().unwrap_or_default(),
524 "tool_calls": calls,
525 "model_id": message.metadata.get("grok_model_id")
526 .or(self.meta.model.as_ref())
527 .cloned()
528 .unwrap_or_else(|| "unknown".to_string()),
529 });
530 if let Some(object) = value.as_object_mut() {
531 for (metadata, field) in [
532 ("grok_model_fingerprint", "model_fingerprint"),
533 ("grok_reasoning_effort", "reasoning_effort"),
534 ] {
535 if let Some(raw) = message.metadata.get(metadata) {
536 object.insert(field.to_string(), Value::String(raw.clone()));
537 }
538 }
539 }
540 value
541 }
542 Role::Tool => {
543 let mut value = serde_json::json!({
544 "type": "tool_result",
545 "tool_call_id": message.tool_call_id.clone().unwrap_or_default(),
546 "content": message.content.clone().unwrap_or_default(),
547 });
548 let images: Vec<Value> = message
549 .content_parts
550 .iter()
551 .flatten()
552 .filter(|part| {
553 part.get("type").and_then(Value::as_str) == Some("image_url")
554 })
555 .filter_map(|part| {
556 part.get("image_url")
557 .and_then(|u| u.get("url"))
558 .and_then(Value::as_str)
559 })
560 .map(|url| serde_json::json!({"type": "image", "url": url}))
561 .collect();
562 if !images.is_empty() {
563 value["images"] = Value::Array(images);
564 }
565 value
566 }
567 };
568 set_grok_target_message_extension(&mut value, message);
569 push_jsonl(out, &value);
570 }
571 }
572
573 pub(super) fn to_grok_jsonl_spliced(&self) -> String {
577 let (raw_prefix_len, message_prefix_len) = self.spliced_prefix_lens();
578 if raw_prefix_len == 0 {
579 return self.to_grok_jsonl();
580 }
581 let mut out = String::new();
582 for line in &self.raw[..raw_prefix_len] {
583 out.push_str(line);
584 out.push('\n');
585 }
586 self.write_grok_records(&mut out, &self.messages[message_prefix_len..]);
587 out
588 }
589}