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