Skip to main content

supercode_interchange/session/
residue.rs

1//! The portable residue as the complement of a projection (`docs/plans/portable-residue.md`).
2//!
3//! A translation keeps what the target format can say; the residue is the rest of the source.
4//! A source file is cut into **segments**, one per message group (a message and the tool results
5//! answering it), each holding the verbatim source records from the record that created the group
6//! up to the next group's creating record. Segments are keyed by a hash chain over the groups'
7//! canonical form, which every format reproduces, so a session read back from any format finds its
8//! source's segments and restores that prefix byte for byte; messages past the restored prefix are
9//! written by the format's own splice writer.
10
11use super::*;
12
13/// Loader-internal: the index in `raw` of the record that created a message, until the loader
14/// moves it into [`SessionMeta::message_records`] with [`take_message_records`].
15const RECORD_KEY: &str = "__supercode_record";
16
17/// Stamp the messages created since the last stamp with `record`. They are always a suffix: a
18/// loader appends, or rebuilds the list outright (Codex's `compacted` replacement history).
19pub(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
30/// Remove the stamps and return them in message order; a message without one (added after
31/// parsing, e.g. a synthesized tool result) has `None`.
32pub(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/// One stored segment: the verbatim source lines of a message group.
45#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
46pub struct ResidueSegment {
47    /// The verbatim source lines, in order.
48    pub lines: Vec<String>,
49    /// The segment ends its source file (whose trailing newline it records).
50    pub end_of_source: bool,
51    /// Whether that source file ended with a newline.
52    pub trailing_newline: bool,
53    /// BLAKE3 over the lines, checked before a segment is used: a segment altered or damaged in
54    /// the store is treated as missing.
55    #[serde(default)]
56    pub digest: String,
57    /// The source session the lines were cut from. Sessions can share a conversation prefix
58    /// (sub-agents opened with one prompt, forks), so a key can hold segments of several sources,
59    /// and a restoration takes all of its segments from one.
60    #[serde(default)]
61    pub source: String,
62    /// That source's session id, when it has one.
63    #[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    /// Whether the lines are the ones the segment was cut with.
87    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/// A segment ready to store: its chain key, its source format and its lines.
102#[derive(Debug, Clone)]
103pub struct KeyedSegment {
104    /// The chain key of the segment's message group.
105    pub key: String,
106    /// The format the lines are in.
107    pub format: SessionFormat,
108    /// The lines themselves.
109    pub segment: ResidueSegment,
110}
111
112/// The message groups of `messages`: each non-tool message opens a group; tool results join the
113/// group of the calls they answer.
114fn 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
140/// The canonical form of a group: what every format carries of it. Tool results are ordered by
141/// the calls they answer, since formats store parallel results in call or completion order.
142fn 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
169/// The key before any group: an empty conversation's only key. Sessions with no messages share
170/// it, so only their session id tells them apart.
171fn empty_conversation_key() -> String {
172    blake3::hash(b"supercode-residue-v1").to_hex().to_string()
173}
174
175/// The chain key of every group: `key(g) = H(key(g-1), canonical(g))`.
176pub 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    /// This session's segments, when its `raw` is its verbatim source and its loader recorded
196    /// which record created each message. Segments stop at the first group whose creating record
197    /// is unknown; the first segment also holds every record before the first message.
198    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        // The source's identity: its format, its id and its first record.
207        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    /// Write this session as `format`, restoring the longest prefix whose segments `lookup` holds
271    /// for that format verbatim and writing the rest with the format's splice writer. `None` when
272    /// no prefix is found; the caller writes the session as usual.
273    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        // A segment that fails the soundness check costs only itself and what follows: retry
292        // with the prefix before the first group that did not read back.
293        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    /// The one source session to restore from: never another session with a different recorded
312    /// id; the one whose segments match the longest prefix of this conversation, then the one with
313    /// this session's id. `None` when no source matches, or when sources tie with different lines
314    /// — which session this is cannot be told apart.
315    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        // An identical conversation from another session is not this session: its lines carry
324        // that session's id and workspace. Only an id the session's own records carry tells
325        // sessions apart; Grok's is its directory, which a materialization mints anew.
326        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    /// A session with no messages: restored only when its id, or a single stored candidate,
379    /// names which empty session it is.
380    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            // `spliced_prefix_lens` reads the raw prefix as `raw.len()` minus one line per
450            // appended message; the placeholders stand for those messages and are never written.
451            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        // Soundness: what is written must read back as exactly this conversation. A segment that
458        // does not (tampered, stale, or a loader that reads its own lines differently) is not used.
459        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/// How much of a written session came from its source's segments.
491#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
492pub struct ResidueRestore {
493    /// Messages whose source records were restored verbatim.
494    pub restored_messages: usize,
495    /// Messages after the restored prefix, written by the format's own writer.
496    pub rendered_messages: usize,
497}