Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 9 additions & 79 deletions crates/tui/src/compaction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -160,17 +160,18 @@ by that language model. Use this to build on the work that has already been done
duplicating work. Here is the summary produced by the other language model, use the information \
in this summary to assist with your own analysis:";

/// Detection marker for committed compaction-summary text: the stable first
/// sentence of `SUMMARY_HEADER`. `engine/context.rs` restores summaries by
/// the same marker on session load.
pub const COMPACTION_SUMMARY_MARKER: &str = "Another language model started to solve this problem";
/// Marker written by pre-v0.9.6 compaction; sessions saved under the old
/// format must still be recognized so their summary is replaced, not stacked.
pub const LEGACY_COMPACTION_SUMMARY_MARKER: &str = "Conversation Summary (Auto-Generated)";
const COMPACTION_CHECKPOINT_PROVENANCE: &str = "<!-- codewhale.compaction-checkpoint.v1 -->";
const COMPACTION_SUMMARY_BEGIN: &str = "<!-- compaction-summary:begin -->";
const COMPACTION_SUMMARY_END: &str = "<!-- compaction-summary:end -->";

// The checkpoint-recognition family lives in the `history_recognition` leaf so
// `compaction` and `runtime_handoff` can share it without a module cycle; the
// names below stay reachable on the `compaction` path.
pub(crate) use crate::history_recognition::{
COMPACTION_CHECKPOINT_PROVENANCE, is_compaction_checkpoint_message,
is_generated_compaction_checkpoint,
};
pub use crate::history_recognition::{COMPACTION_SUMMARY_MARKER, LEGACY_COMPACTION_SUMMARY_MARKER};

/// Whether a system-prompt text block is a committed compaction summary.
#[must_use]
pub fn is_compaction_summary_text(text: &str) -> bool {
Expand Down Expand Up @@ -296,77 +297,6 @@ pub(crate) fn compaction_checkpoint_message(prompt: &SystemPrompt) -> Message {
}
}

/// Whether the first text block carries the summary header, current or legacy.
fn has_compaction_summary_header(text: &str) -> bool {
// `COMPACTION_SUMMARY_MARKER` is the current header's first sentence, and
// releases before this one also committed carriers with their own body
// after that sentence, so the prefix — not the whole header — is the
// stable shape.
text.starts_with(COMPACTION_SUMMARY_MARKER)
|| text.starts_with(LEGACY_COMPACTION_SUMMARY_MARKER)
}

/// Structural recognition of the one generated checkpoint in saved history.
///
/// The marker substring scan above stays scoped to system-prompt carriers:
/// on history it matches an ordinary user turn that merely *quotes* the
/// header, and every consumer here either deletes or replaces what it matches.
/// Structure instead — a `role="user"` message whose first text block begins
/// with the header and whose remaining block, if any, is exactly the
/// engine-written provenance marker.
///
/// Boundary: the single-block form has no provenance to check, because
/// releases before this one saved the bare summary, and a reload that failed
/// to recognize it would stack a second summary beside it. A user turn that
/// *begins* with the header is therefore still read as a carrier here. Request
/// rewriting does not share that reading — see
/// [`is_generated_compaction_checkpoint`] — so such a turn is never moved or
/// merged on the wire.
#[must_use]
pub(crate) fn is_compaction_checkpoint_message(message: &Message) -> bool {
let [
ContentBlock::Text {
text,
cache_control: None,
},
rest @ ..,
] = message.content.as_slice()
else {
return false;
};
message.role == Role::User
&& has_compaction_summary_header(text)
&& (rest.is_empty()
|| matches!(
rest,
[ContentBlock::Text {
text: provenance,
cache_control: None,
}] if provenance == COMPACTION_CHECKPOINT_PROVENANCE
))
}

/// The provenance-stamped form, and the only form request rewriting may
/// relocate or merge. A user cannot type this shape, so an ordinary turn that
/// pastes the whole summary header after a tool result keeps its position.
#[must_use]
pub(crate) fn is_generated_compaction_checkpoint(message: &Message) -> bool {
let [
ContentBlock::Text {
cache_control: None,
..
},
ContentBlock::Text {
text: provenance,
cache_control: None,
},
] = message.content.as_slice()
else {
return false;
};
provenance == COMPACTION_CHECKPOINT_PROVENANCE && is_compaction_checkpoint_message(message)
}

/// Replace the saved history checkpoint with the authoritative carrier, keep
/// its position relative to later turns, and repair a pre-placement-fix
/// Agent-topology sidecar. Both steps exist so the first request after a
Expand Down
94 changes: 94 additions & 0 deletions crates/tui/src/history_recognition.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
//! Structural recognition of the compaction checkpoint in saved history.
//!
//! Leaf module shared by `compaction` and `runtime_handoff`. Hosting the
//! checkpoint-recognition family here keeps `runtime_handoff`'s production
//! code from importing `compaction` (its tests still build carriers through
//! `compaction`'s constructors): `compaction` already imports
//! `runtime_handoff` for restore-time topology relocation, so a reverse edge
//! would close a module dependency cycle.

use crate::models::{ContentBlock, Message, Role};

/// Detection marker for committed compaction-summary text: the stable first
/// sentence of the summary header `compaction` commits (`SUMMARY_HEADER`).
/// `engine/context.rs` restores summaries by the same marker on session load.
pub const COMPACTION_SUMMARY_MARKER: &str = "Another language model started to solve this problem";
/// Marker written by pre-v0.9.6 compaction; sessions saved under the old
/// format must still be recognized so their summary is replaced, not stacked.
pub const LEGACY_COMPACTION_SUMMARY_MARKER: &str = "Conversation Summary (Auto-Generated)";
pub(crate) const COMPACTION_CHECKPOINT_PROVENANCE: &str =
"<!-- codewhale.compaction-checkpoint.v1 -->";

/// Whether the first text block carries the summary header, current or legacy.
fn has_compaction_summary_header(text: &str) -> bool {
// `COMPACTION_SUMMARY_MARKER` is the current header's first sentence, and
// releases before this one also committed carriers with their own body
// after that sentence, so the prefix — not the whole header — is the
// stable shape.
text.starts_with(COMPACTION_SUMMARY_MARKER)
|| text.starts_with(LEGACY_COMPACTION_SUMMARY_MARKER)
}

/// Structural recognition of the one generated checkpoint in saved history.
///
/// The marker substring scan in `compaction` (`is_compaction_summary_text`)
/// stays scoped to system-prompt carriers:
/// on history it matches an ordinary user turn that merely *quotes* the
/// header, and the consumers that match on history — `compaction` restore,
/// `runtime_handoff` placement and edit-guarding — replace what they match
/// or must protect it from deletion.
/// Structure instead — a `role="user"` message whose first text block begins
/// with the header and whose remaining block, if any, is exactly the
/// engine-written provenance marker.
///
/// Boundary: the single-block form has no provenance to check, because
/// releases before this one saved the bare summary, and a reload that failed
/// to recognize it would stack a second summary beside it. A user turn that
/// *begins* with the header is therefore still read as a carrier here. Request
/// rewriting does not share that reading — see
/// [`is_generated_compaction_checkpoint`] — so such a turn is never moved or
/// merged on the wire.
#[must_use]
pub(crate) fn is_compaction_checkpoint_message(message: &Message) -> bool {
let [
ContentBlock::Text {
text,
cache_control: None,
},
rest @ ..,
] = message.content.as_slice()
else {
return false;
};
message.role == Role::User
&& has_compaction_summary_header(text)
&& (rest.is_empty()
|| matches!(
rest,
[ContentBlock::Text {
text: provenance,
cache_control: None,
}] if provenance == COMPACTION_CHECKPOINT_PROVENANCE
))
}

/// The provenance-stamped form, and the only form request rewriting may
/// relocate or merge. A user cannot type this shape, so an ordinary turn that
/// pastes the whole summary header after a tool result keeps its position.
#[must_use]
pub(crate) fn is_generated_compaction_checkpoint(message: &Message) -> bool {
let [
ContentBlock::Text {
cache_control: None,
..
},
ContentBlock::Text {
text: provenance,
cache_control: None,
},
] = message.content.as_slice()
else {
return false;
};
provenance == COMPACTION_CHECKPOINT_PROVENANCE && is_compaction_checkpoint_message(message)
}
1 change: 1 addition & 0 deletions crates/tui/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ pub use fleet::profile::WORKSPACE_AGENT_PROFILE_DIR;
pub use fleet::roster::FleetRoster;
mod goal_loop;
mod hashing;
mod history_recognition;
#[doc(hidden)]
pub mod hooks;
mod image_attach;
Expand Down
8 changes: 4 additions & 4 deletions crates/tui/src/runtime_handoff.rs
Original file line number Diff line number Diff line change
Expand Up @@ -805,7 +805,7 @@ pub(crate) fn replace_agent_topology_checkpoint(
let ends_with_tool_result = messages.last().is_some_and(carries_tool_result);
let ends_with_compaction_summary = messages
.last()
.is_some_and(crate::compaction::is_generated_compaction_checkpoint);
.is_some_and(crate::history_recognition::is_generated_compaction_checkpoint);
let position = if ends_with_tool_result || ends_with_compaction_summary {
compaction_anchor(messages, messages.len()).map_or(0, CompactionAnchor::placement_index)
} else {
Expand Down Expand Up @@ -1328,7 +1328,7 @@ fn is_compaction_topology_carrier(message: &Message) -> bool {
fn restored_topology_anchor(messages: &[Message], index: usize) -> Option<usize> {
let summary_before = messages[..index]
.iter()
.rposition(crate::compaction::is_compaction_checkpoint_message);
.rposition(crate::history_recognition::is_compaction_checkpoint_message);
let follows_tool_result = index > 0 && carries_tool_result(&messages[index - 1]);
if summary_before.is_none() && !follows_tool_result {
return None;
Expand All @@ -1337,7 +1337,7 @@ fn restored_topology_anchor(messages: &[Message], index: usize) -> Option<usize>
.or_else(|| {
messages[index + 1..]
.iter()
.position(crate::compaction::is_compaction_checkpoint_message)
.position(crate::history_recognition::is_compaction_checkpoint_message)
.map(|offset| index + 1 + offset)
})
.unwrap_or(messages.len());
Expand Down Expand Up @@ -1480,7 +1480,7 @@ fn is_runtime_owned_user_message(message: &Message) -> bool {
// The compaction checkpoint is engine-written history, so `/edit` must
// not treat it as the turn to truncate at: doing so deletes the summary
// the session is built on. Structure decides, not the marker substring.
|| crate::compaction::is_compaction_checkpoint_message(message)
|| crate::history_recognition::is_compaction_checkpoint_message(message)
}

/// Return engine-owned metadata in either the current trailing shape or the
Expand Down
Loading