1use super::*;
12
13const RECORD_KEY: &str = "__supercode_record";
16
17pub(super) fn stamp_message_records(messages: &mut [ChatMessage], record: usize) {
20 for message in messages.iter_mut().rev() {
21 if message.metadata.contains_key(RECORD_KEY) {
22 break;
23 }
24 message
25 .metadata
26 .insert(RECORD_KEY.to_string(), record.to_string());
27 }
28}
29
30pub(super) fn take_message_records(messages: &mut [ChatMessage]) -> Vec<Option<usize>> {
33 messages
34 .iter_mut()
35 .map(|message| {
36 message
37 .metadata
38 .remove(RECORD_KEY)
39 .and_then(|record| record.parse().ok())
40 })
41 .collect()
42}
43
44#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
46pub struct ResidueSegment {
47 pub lines: Vec<String>,
49 pub end_of_source: bool,
51 pub trailing_newline: bool,
53 #[serde(default)]
56 pub digest: String,
57 #[serde(default)]
61 pub source: String,
62 #[serde(default)]
64 pub source_session: Option<String>,
65}
66
67impl ResidueSegment {
68 fn new(
69 lines: Vec<String>,
70 end_of_source: bool,
71 trailing_newline: bool,
72 source: &str,
73 source_session: Option<&str>,
74 ) -> Self {
75 let digest = segment_digest(&lines);
76 Self {
77 lines,
78 end_of_source,
79 trailing_newline,
80 digest,
81 source: source.to_string(),
82 source_session: source_session.map(str::to_string),
83 }
84 }
85
86 pub fn verified(&self) -> bool {
88 self.digest == segment_digest(&self.lines)
89 }
90}
91
92fn segment_digest(lines: &[String]) -> String {
93 let mut hasher = blake3::Hasher::new();
94 for line in lines {
95 hasher.update(&(line.len() as u64).to_le_bytes());
96 hasher.update(line.as_bytes());
97 }
98 hasher.finalize().to_hex().to_string()
99}
100
101#[derive(Debug, Clone)]
103pub struct KeyedSegment {
104 pub key: String,
106 pub format: SessionFormat,
108 pub segment: ResidueSegment,
110}
111
112fn message_groups(messages: &[ChatMessage]) -> Vec<std::ops::Range<usize>> {
115 let mut groups: Vec<std::ops::Range<usize>> = Vec::new();
116 for (index, message) in messages.iter().enumerate() {
117 match groups.last_mut() {
118 Some(group) if message.role == Role::Tool => group.end = index + 1,
119 _ => groups.push(index..index + 1),
120 }
121 }
122 groups
123}
124
125fn message_text(message: &ChatMessage) -> String {
126 match &message.content_parts {
127 Some(parts) => parts
128 .iter()
129 .filter(|part| part.get("type").and_then(Value::as_str) == Some("text"))
130 .filter_map(|part| part.get("text").and_then(Value::as_str))
131 .collect(),
132 None => message.content.clone().unwrap_or_default(),
133 }
134}
135
136fn normalized_arguments(arguments: &str) -> Value {
137 serde_json::from_str(arguments).unwrap_or_else(|_| Value::String(arguments.to_string()))
138}
139
140fn canonical_group(messages: &[ChatMessage]) -> Value {
143 let head = &messages[0];
144 let calls = head.tool_calls();
145 let mut results: Vec<&ChatMessage> = messages[1..].iter().collect();
146 results.sort_by_key(|result| {
147 calls
148 .iter()
149 .position(|call| Some(call.id.as_str()) == result.tool_call_id.as_deref())
150 .unwrap_or(usize::MAX)
151 });
152 serde_json::json!([
153 serde_json::to_value(head.role).unwrap_or(Value::Null),
154 message_text(head),
155 calls
156 .iter()
157 .map(|call| serde_json::json!([
158 call.function.name,
159 normalized_arguments(&call.function.arguments)
160 ]))
161 .collect::<Vec<_>>(),
162 results
163 .iter()
164 .map(|result| message_text(result))
165 .collect::<Vec<_>>(),
166 ])
167}
168
169fn empty_conversation_key() -> String {
172 blake3::hash(b"supercode-residue-v1").to_hex().to_string()
173}
174
175pub fn residue_chain_keys(messages: &[ChatMessage]) -> Vec<(std::ops::Range<usize>, String)> {
177 let mut previous = empty_conversation_key();
178 message_groups(messages)
179 .into_iter()
180 .map(|group| {
181 let mut hasher = blake3::Hasher::new();
182 hasher.update(previous.as_bytes());
183 hasher.update(
184 canonical_group(&messages[group.clone()])
185 .to_string()
186 .as_bytes(),
187 );
188 previous = hasher.finalize().to_hex().to_string();
189 (group, previous.clone())
190 })
191 .collect()
192}
193
194impl Session {
195 pub fn residue_segments(&self) -> Vec<KeyedSegment> {
199 let Some(format) = SessionFormat::for_source(self.meta.source) else {
200 return Vec::new();
201 };
202 if !self.raw_is_verbatim || self.meta.message_records.len() != self.messages.len() {
203 return Vec::new();
204 }
205 let keyed = residue_chain_keys(&self.messages);
206 let source = {
208 let mut hasher = blake3::Hasher::new();
209 hasher.update(format!("{format:?}").as_bytes());
210 hasher.update(self.meta.session_id.as_deref().unwrap_or("").as_bytes());
211 hasher.update(
212 self.raw
213 .first()
214 .map(String::as_str)
215 .unwrap_or("")
216 .as_bytes(),
217 );
218 hasher.finalize().to_hex().to_string()
219 };
220 if keyed.is_empty() {
221 if self.raw.is_empty() {
222 return Vec::new();
223 }
224 return vec![KeyedSegment {
225 key: empty_conversation_key(),
226 format,
227 segment: ResidueSegment::new(
228 self.raw.clone(),
229 true,
230 self.raw_trailing_newline,
231 &source,
232 self.meta.session_id.as_deref(),
233 ),
234 }];
235 }
236 let mut starts = Vec::new();
237 for (group, _) in &keyed {
238 match self.meta.message_records[group.start] {
239 Some(record) => starts.push(record),
240 None => break,
241 }
242 }
243 let mut segments = Vec::new();
244 for (index, (_, key)) in keyed.iter().take(starts.len()).enumerate() {
245 let from = if index == 0 { 0 } else { starts[index] };
246 let end_of_source = index + 1 == keyed.len();
247 let to = if end_of_source {
248 self.raw.len()
249 } else {
250 starts.get(index + 1).copied().unwrap_or(self.raw.len())
251 };
252 if from > to || to > self.raw.len() {
253 break;
254 }
255 segments.push(KeyedSegment {
256 key: key.clone(),
257 format,
258 segment: ResidueSegment::new(
259 self.raw[from..to].to_vec(),
260 end_of_source,
261 self.raw_trailing_newline,
262 &source,
263 self.meta.session_id.as_deref(),
264 ),
265 });
266 }
267 segments
268 }
269
270 pub fn restore_residue(
274 &self,
275 format: SessionFormat,
276 lookup: impl Fn(SessionFormat, &str) -> Vec<ResidueSegment>,
277 ) -> Result<Option<(String, ResidueRestore)>> {
278 let keyed = residue_chain_keys(&self.messages);
279 if keyed.is_empty() {
280 return Ok(self.restore_empty_conversation(format, &lookup));
281 }
282 let Some(source) = self.residue_source(format, &lookup, &keyed) else {
283 return Ok(None);
284 };
285 let lookup = |format: SessionFormat, key: &str| {
286 lookup(format, key)
287 .into_iter()
288 .find(|segment| segment.source == source && segment.verified())
289 };
290 let mut limit = keyed.len();
291 loop {
294 match self.restore_prefix(format, &lookup, &keyed, limit)? {
295 Restored::Sound(text, report) => return Ok(Some((text, report))),
296 Restored::Nothing => return Ok(None),
297 Restored::Unsound {
298 first_mismatch,
299 restored_groups,
300 } => {
301 let shorter = first_mismatch.min(restored_groups.saturating_sub(1));
302 if shorter == 0 || shorter >= limit {
303 return Ok(None);
304 }
305 limit = shorter;
306 }
307 }
308 }
309 }
310
311 fn residue_source(
316 &self,
317 format: SessionFormat,
318 lookup: &impl Fn(SessionFormat, &str) -> Vec<ResidueSegment>,
319 keyed: &[(std::ops::Range<usize>, String)],
320 ) -> Option<String> {
321 let first = keyed.first()?;
322 let own = self.meta.session_id.as_deref();
323 let own_recorded = own.filter(|_| self.meta.source != SessionSource::Grok);
327 let mut best: Vec<(String, Option<String>, usize, Vec<String>)> = Vec::new();
328 let candidates: Vec<ResidueSegment> = lookup(format, &first.1)
329 .into_iter()
330 .filter(ResidueSegment::verified)
331 .filter(|candidate| {
332 own_recorded.is_none()
333 || candidate.source_session.is_none()
334 || candidate.source_session.as_deref() == own_recorded
335 })
336 .collect();
337 for candidate in &candidates {
338 let mut matched = 0;
339 let mut lines = Vec::new();
340 for (_, key) in keyed {
341 let Some(segment) = lookup(format, key)
342 .into_iter()
343 .find(|segment| segment.source == candidate.source && segment.verified())
344 else {
345 break;
346 };
347 matched += 1;
348 let end = segment.end_of_source;
349 lines.extend(segment.lines);
350 if end {
351 break;
352 }
353 }
354 best.push((
355 candidate.source.clone(),
356 candidate.source_session.clone(),
357 matched,
358 lines,
359 ));
360 }
361 let longest = best.iter().map(|(_, _, matched, _)| *matched).max()?;
362 best.retain(|(_, _, matched, _)| *matched == longest);
363 if best.len() > 1 {
364 if own.is_some()
365 && best
366 .iter()
367 .any(|(_, session, _, _)| session.as_deref() == own)
368 {
369 best.retain(|(_, session, _, _)| session.as_deref() == own);
370 }
371 }
372 let (source, _, _, lines) = best.first()?;
373 best.iter()
374 .all(|(_, _, _, other)| other == lines)
375 .then(|| source.clone())
376 }
377
378 fn restore_empty_conversation(
381 &self,
382 format: SessionFormat,
383 lookup: &impl Fn(SessionFormat, &str) -> Vec<ResidueSegment>,
384 ) -> Option<(String, ResidueRestore)> {
385 let mut candidates: Vec<ResidueSegment> = lookup(format, &empty_conversation_key())
386 .into_iter()
387 .filter(ResidueSegment::verified)
388 .collect();
389 let own = self.meta.session_id.as_deref();
390 if candidates.len() > 1 && own.is_some() {
391 candidates.retain(|segment| segment.source_session.as_deref() == own);
392 }
393 let [segment] = candidates.as_slice() else {
394 return None;
395 };
396 let mut text = segment.lines.join("\n");
397 if segment.trailing_newline {
398 text.push('\n');
399 }
400 let written = Session::load_str(&text, format).ok()?;
401 written.messages.is_empty().then_some((
402 text,
403 ResidueRestore {
404 restored_messages: 0,
405 rendered_messages: 0,
406 },
407 ))
408 }
409
410 fn restore_prefix(
411 &self,
412 format: SessionFormat,
413 lookup: &impl Fn(SessionFormat, &str) -> Option<ResidueSegment>,
414 keyed: &[(std::ops::Range<usize>, String)],
415 limit: usize,
416 ) -> Result<Restored> {
417 let mut lines = Vec::new();
418 let mut restored_groups = 0;
419 let mut restored_messages = 0;
420 let mut whole_source = None;
421 for (group, key) in keyed.iter().take(limit) {
422 let Some(segment) = lookup(format, key) else {
423 break;
424 };
425 lines.extend(segment.lines);
426 restored_groups += 1;
427 restored_messages = group.end;
428 if segment.end_of_source {
429 whole_source = Some(segment.trailing_newline);
430 break;
431 }
432 }
433 if restored_groups == 0 {
434 return Ok(Restored::Nothing);
435 }
436 let report = ResidueRestore {
437 restored_messages,
438 rendered_messages: self.messages.len() - restored_messages,
439 };
440 let text = if let (Some(trailing_newline), 0) = (whole_source, report.rendered_messages) {
441 let mut text = lines.join("\n");
442 if trailing_newline {
443 text.push('\n');
444 }
445 text
446 } else {
447 let mut spliced = self.clone();
448 spliced.meta.source = format.source();
449 lines.extend(std::iter::repeat_n(String::new(), report.rendered_messages));
452 spliced.raw = lines;
453 spliced.raw_is_verbatim = true;
454 spliced.imported_message_count = Some(restored_messages);
455 spliced.to_jsonl_spliced(format, None)?
456 };
457 let Ok(written) = Session::load_str(&text, format) else {
460 return Ok(Restored::Unsound {
461 first_mismatch: 0,
462 restored_groups,
463 });
464 };
465 let actual = residue_chain_keys(&written.messages);
466 let first_mismatch = keyed
467 .iter()
468 .zip(actual.iter())
469 .position(|((_, expected), (_, actual))| expected != actual)
470 .unwrap_or_else(|| keyed.len().min(actual.len()));
471 if first_mismatch < keyed.len() || actual.len() != keyed.len() {
472 return Ok(Restored::Unsound {
473 first_mismatch,
474 restored_groups,
475 });
476 }
477 Ok(Restored::Sound(text, report))
478 }
479}
480
481enum Restored {
482 Sound(String, ResidueRestore),
483 Unsound {
484 first_mismatch: usize,
485 restored_groups: usize,
486 },
487 Nothing,
488}
489
490#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
492pub struct ResidueRestore {
493 pub restored_messages: usize,
495 pub rendered_messages: usize,
497}