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 restore_grok_message_extension(&value, &mut message);
151 messages.push(message);
152 }
153 _ => {}
157 }
158 }
159
160 ensure_tool_results_paired(&mut messages);
161 let imported_message_count = Some(messages.len());
162 Ok(Session {
163 meta,
164 messages,
165 subagents: Vec::new(),
166 raw,
167 raw_trailing_newline,
168 imported_message_count,
169 raw_is_verbatim: true,
170 parse_error_lines,
171 load_residue: Vec::new(),
172 })
173 }
174
175 pub(super) fn capture_grok_path_metadata(&mut self, transcript: &Path) {
176 let Some(session_dir) = transcript.parent() else {
177 return;
178 };
179 self.meta.session_id = session_dir
180 .file_name()
181 .and_then(|name| name.to_str())
182 .map(str::to_string);
183 self.meta.cwd = session_dir
184 .parent()
185 .and_then(Path::file_name)
186 .and_then(|name| name.to_str())
187 .and_then(percent_decode_path)
188 .map(PathBuf::from);
189
190 let Ok(summary_text) = std::fs::read_to_string(session_dir.join("summary.json")) else {
191 return;
192 };
193 let Ok(summary) = serde_json::from_str::<Value>(&summary_text) else {
194 return;
195 };
196 if let Some(model) = summary.get("current_model_id").and_then(Value::as_str) {
197 self.meta.model = Some(model.to_string());
198 }
199 for (source, target) in [
200 ("generated_title", "session_name"),
201 ("created_at", "created_at"),
202 ("updated_at", "updated_at"),
203 ("chat_format_version", "grok_chat_format_version"),
204 ] {
205 if let Some(value) = summary.get(source) {
206 self.meta.lineage.insert(
207 target.to_string(),
208 value
209 .as_str()
210 .map(str::to_string)
211 .unwrap_or_else(|| value.to_string()),
212 );
213 }
214 }
215 }
216}
217
218const GROK_CONSUMED_TYPES: [&str; 4] = ["system", "user", "assistant", "tool_result"];
222
223pub(super) fn grok_residue_kind(record: &Value) -> Option<String> {
224 record.as_object()?;
225 match record.get("type").and_then(Value::as_str) {
226 Some(kind) if !GROK_CONSUMED_TYPES.contains(&kind) => Some(kind.to_string()),
227 Some(_) => None,
228 None => Some("untyped".to_string()),
229 }
230}
231
232fn capture_grok_scalar_metadata(value: &Value, message: &mut ChatMessage, keys: &[&str]) {
235 for key in keys {
236 if let Some(value) = value.get(*key) {
237 message.metadata.insert(
238 format!("grok_{key}"),
239 value
240 .as_str()
241 .map(str::to_string)
242 .unwrap_or_else(|| value.to_string()),
243 );
244 }
245 }
246}
247
248fn grok_human_user_text(raw: &str) -> Option<String> {
249 let text = raw.trim();
250 if text.is_empty() || text.starts_with("<user_info>") || text.starts_with("<system-reminder>") {
251 return None;
252 }
253 let unwrapped = text
254 .strip_prefix("<user_query>")
255 .and_then(|value| value.strip_suffix("</user_query>"))
256 .map(str::trim)
257 .unwrap_or(text);
258 (!unwrapped.is_empty()).then(|| unwrapped.to_string())
259}
260
261const SUPERCODE_GROK_MESSAGE_KEY: &str = "_supercode_grok_message";
269
270fn grok_message_extension(source: SessionSource, message: &ChatMessage) -> Option<Value> {
271 let metadata = message
272 .metadata
273 .iter()
274 .filter(|(key, _)| {
275 key.starts_with("gemini_") || key.starts_with("grok_") || key.starts_with("goose_")
276 })
277 .map(|(key, value)| (key.clone(), Value::String(value.clone())))
278 .collect::<serde_json::Map<_, _>>();
279
280 let has_portable_fields =
285 !metadata.is_empty() || message.metadata.contains_key("codex_custom_tool_call_ids");
286 (matches!(
287 source,
288 SessionSource::Gemini | SessionSource::Grok | SessionSource::Goose
289 ) || has_portable_fields
290 || message.content_parts.is_some())
291 .then(|| {
292 serde_json::json!({
293 "schema": 2,
294 "role": message.role,
295 "content": message.content,
296 "content_parts": message.content_parts,
297 "tool_calls": message.tool_calls,
298 "tool_call_id": message.tool_call_id,
299 "name": message.name,
300 "metadata": message.metadata,
301 })
302 })
303}
304
305pub(super) fn set_grok_target_message_extension(value: &mut Value, message: &ChatMessage) {
306 value[SUPERCODE_GROK_MESSAGE_KEY] = serde_json::json!({
307 "schema": 2,
308 "role": message.role,
309 "content": message.content,
310 "content_parts": message.content_parts,
311 "tool_calls": message.tool_calls,
312 "tool_call_id": message.tool_call_id,
313 "name": message.name,
314 "metadata": message.metadata,
315 });
316}
317
318pub(super) fn set_grok_message_extension(
319 value: &mut Value,
320 source: SessionSource,
321 message: &ChatMessage,
322) {
323 if let Some(extension) = grok_message_extension(source, message) {
324 value[SUPERCODE_GROK_MESSAGE_KEY] = extension;
325 }
326}
327
328pub(super) fn restore_grok_message_extension(value: &Value, message: &mut ChatMessage) {
329 let Some(extension) = value.get(SUPERCODE_GROK_MESSAGE_KEY) else {
330 return;
331 };
332 let codex_open_turn = message.metadata.get("__codex_open_turn").cloned();
337 let codex_turn_id = message.metadata.get("turn_id").cloned();
338 let extension_has_turn_id = extension
339 .get("metadata")
340 .and_then(Value::as_object)
341 .is_some_and(|metadata| metadata.contains_key("turn_id"));
342 if extension.get("schema").and_then(Value::as_u64) == Some(2) {
343 if let Some(role) = extension
344 .get("role")
345 .and_then(|value| serde_json::from_value(value.clone()).ok())
346 {
347 message.role = role;
348 }
349 message.content = extension
350 .get("content")
351 .and_then(Value::as_str)
352 .map(str::to_string);
353 message.content_parts = extension
354 .get("content_parts")
355 .and_then(|value| serde_json::from_value(value.clone()).ok());
356 message.tool_call_id = extension
362 .get("tool_call_id")
363 .and_then(Value::as_str)
364 .map(str::to_string);
365 message.name = extension
366 .get("name")
367 .and_then(Value::as_str)
368 .map(str::to_string);
369 message.metadata.clear();
370 }
371 if let Some(metadata) = extension.get("metadata").and_then(Value::as_object) {
372 for (key, value) in metadata {
373 if let Some(value) = value.as_str() {
374 message.metadata.insert(key.clone(), value.to_string());
375 }
376 }
377 }
378 if let Some(name) = extension.get("name").and_then(Value::as_str) {
379 message.name = Some(name.to_string());
380 }
381 if let Some(marker) = codex_open_turn {
382 message
383 .metadata
384 .insert("__codex_open_turn".to_string(), marker);
385 }
386 if let Some(turn_id) = codex_turn_id {
387 message.metadata.insert("turn_id".to_string(), turn_id);
388 if !extension_has_turn_id {
389 message.metadata.insert(
390 "__grok_remove_synthetic_turn_id".to_string(),
391 "true".to_string(),
392 );
393 }
394 }
395}
396
397pub(super) fn restore_single_grok_message(value: &Value, messages: &mut [ChatMessage]) {
398 if let [message] = messages {
399 restore_grok_message_extension(value, message);
400 }
401}
402
403impl Session {
404 pub(super) fn to_grok_jsonl(&self) -> String {
408 let mut out = String::new();
409 if let Some(prompt) = self
410 .meta
411 .system_prompt
412 .as_deref()
413 .filter(|prompt| !prompt.is_empty())
414 {
415 push_jsonl(
416 &mut out,
417 &serde_json::json!({
418 "type": "system",
419 "content": prompt,
420 }),
421 );
422 }
423 self.write_grok_records(&mut out, &self.messages);
424 if self.meta.native_residue_source.as_deref() == Some("grok") {
428 let mut records: Vec<&Value> = self.meta.native_residue.iter().collect();
429 records.sort_by_key(|entry| {
430 entry
431 .get("record_index")
432 .and_then(Value::as_u64)
433 .unwrap_or(u64::MAX)
434 });
435 for entry in records {
436 if let Some(raw) = entry.get("raw").and_then(Value::as_str) {
437 out.push_str(raw);
438 out.push('\n');
439 }
440 }
441 } else if let Some(extension) = native_residue_envelope(&self.meta) {
442 if out.is_empty() {
443 push_jsonl(
444 &mut out,
445 &serde_json::json!({"type": "system", "content": ""}),
446 );
447 }
448 inject_first_jsonl_top_level(
449 &mut out,
450 SUPERCODE_NATIVE_RESIDUE_SUMMARY_KEY,
451 native_residue_summary(&extension),
452 );
453 inject_first_jsonl_top_level(&mut out, SUPERCODE_NATIVE_RESIDUE_KEY, extension);
454 }
455 out
456 }
457
458 fn write_grok_records(&self, out: &mut String, messages: &[ChatMessage]) {
459 for message in messages {
460 if is_replay_excluded(message) {
461 continue;
462 }
463 let mut value = match message.role {
464 Role::System => serde_json::json!({
465 "type": "user",
466 "content": [{"type": "text", "text": message.content.clone().unwrap_or_default()}],
467 "synthetic_reason": "supercode_system_event",
468 }),
469 Role::User => {
470 let mut value = serde_json::json!({
471 "type": "user",
472 "content": [{"type": "text", "text": message.content.clone().unwrap_or_default()}],
473 });
474 if let Some(object) = value.as_object_mut() {
475 for (metadata, field) in [
476 ("grok_prompt_index", "prompt_index"),
477 ("grok_prior_turn_interrupt", "prior_turn_interrupt"),
478 ("grok_synthetic_reason", "synthetic_reason"),
479 ] {
480 if let Some(raw) = message.metadata.get(metadata) {
481 object.insert(
482 field.to_string(),
483 serde_json::from_str(raw)
484 .unwrap_or_else(|_| Value::String(raw.clone())),
485 );
486 }
487 }
488 }
489 value
490 }
491 Role::Assistant => {
492 let calls = message
493 .tool_calls()
494 .iter()
495 .map(|call| {
496 serde_json::json!({
497 "id": call.id,
498 "name": call.function.name,
499 "arguments": call.function.arguments,
500 })
501 })
502 .collect::<Vec<_>>();
503 let mut value = serde_json::json!({
504 "type": "assistant",
505 "content": message.content.clone().unwrap_or_default(),
506 "tool_calls": calls,
507 "model_id": message.metadata.get("grok_model_id")
508 .or(self.meta.model.as_ref())
509 .cloned()
510 .unwrap_or_else(|| "unknown".to_string()),
511 });
512 if let Some(object) = value.as_object_mut() {
513 for (metadata, field) in [
514 ("grok_model_fingerprint", "model_fingerprint"),
515 ("grok_reasoning_effort", "reasoning_effort"),
516 ] {
517 if let Some(raw) = message.metadata.get(metadata) {
518 object.insert(field.to_string(), Value::String(raw.clone()));
519 }
520 }
521 }
522 value
523 }
524 Role::Tool => serde_json::json!({
525 "type": "tool_result",
526 "tool_call_id": message.tool_call_id.clone().unwrap_or_default(),
527 "content": message.content.clone().unwrap_or_default(),
528 }),
529 };
530 set_grok_target_message_extension(&mut value, message);
531 push_jsonl(out, &value);
532 }
533 }
534
535 pub(super) fn to_grok_jsonl_spliced(&self) -> String {
539 let (raw_prefix_len, message_prefix_len) = self.spliced_prefix_lens();
540 if raw_prefix_len == 0 {
541 return self.to_grok_jsonl();
542 }
543 let mut out = String::new();
544 for line in &self.raw[..raw_prefix_len] {
545 out.push_str(line);
546 out.push('\n');
547 }
548 self.write_grok_records(&mut out, &self.messages[message_prefix_len..]);
549 out
550 }
551}