Skip to main content

zagens_core/
compaction.rs

1//! Compaction configuration and artifact types — shared between core and runtime-server.
2
3/// Default model used when no model is specified.
4pub const DEFAULT_COMPACTION_MODEL: &str = "deepseek-chat";
5
6/// Hard floor for automatic compaction in v0.8.11+.
7///
8/// Below this token count, `should_compact` returns `false` regardless of
9/// `enabled` or `token_threshold`. The point of the floor is V4 prefix-cache
10/// economics: compaction rewrites the stable prefix, which destroys the KV
11/// cache.
12///
13/// Manual `/compact` slash command bypasses this floor with explicit user
14/// agency.
15pub const MINIMUM_AUTO_COMPACTION_TOKENS: usize = 500_000;
16
17/// Configuration for conversation compaction behavior.
18#[derive(Debug, Clone, PartialEq)]
19pub struct CompactionConfig {
20    pub enabled: bool,
21    pub token_threshold: usize,
22    pub model: String,
23    pub cache_summary: bool,
24    /// Hard floor — `should_compact` returns `false` when total session
25    /// tokens fall below this number, regardless of `enabled` or
26    /// `token_threshold`.
27    pub auto_floor_tokens: usize,
28}
29
30impl Default for CompactionConfig {
31    fn default() -> Self {
32        Self {
33            enabled: true,
34            token_threshold: 800_000,
35            model: DEFAULT_COMPACTION_MODEL.to_string(),
36            cache_summary: true,
37            auto_floor_tokens: MINIMUM_AUTO_COMPACTION_TOKENS,
38        }
39    }
40}
41
42// ── P2-C: CompactionArtifact ───────────────────────────────────────────────────
43
44/// Opaque artifact identifier (UUID string).
45pub type ArtifactId = String;
46
47/// A compaction artifact — records a single compaction event non-destructively.
48///
49/// **Phase 2-C goal**: compaction no longer destroys message history.  Instead,
50/// the replaced messages are stored here.  The active rendering path skips
51/// `replaced_range` and injects `summary` via the `"memory.compaction"`
52/// `ContextSource`; with a wider budget the original messages can be replayed
53/// verbatim (reversibility gate).
54///
55/// Stored in the `compaction_artifacts` SQLite table (additive migration —
56/// older binaries that do not know about this table simply ignore it).
57///
58/// # Fields
59///
60/// - `id`: stable UUID string, used as the SQLite primary key.
61/// - `session_id`: foreign key into `sessions`.
62/// - `created_at_ms`: wall-clock milliseconds since UNIX epoch at creation.
63/// - `replaced_range`: half-open `[start, end)` index range into the
64///   original `session.messages` vector.  Messages at these indices are
65///   "virtually removed" in the compacted view.
66/// - `replaced_messages_json`: JSON-serialised `Vec<Message>` from
67///   `replaced_range`.  Preserves the original content so the session can be
68///   replayed without the artifact (reversibility).
69/// - `summary`: LLM-generated summary text injected by
70///   `"memory.compaction"` source.
71/// - `original_tokens`: raw token estimate for the replaced messages.
72/// - `summary_tokens`: token estimate for `summary`.
73#[derive(Debug, Clone)]
74pub struct CompactionArtifact {
75    pub id: ArtifactId,
76    pub session_id: String,
77    pub created_at_ms: i64,
78    /// `[start, end)` index range in the original `session.messages` vector.
79    pub replaced_start: usize,
80    pub replaced_end: usize,
81    /// Original messages preserved for reversibility.  Stored as JSON in SQLite.
82    pub replaced_messages_json: String,
83    pub summary: String,
84    pub original_tokens: u32,
85    pub summary_tokens: u32,
86}
87
88impl CompactionArtifact {
89    /// Number of messages replaced by this artifact.
90    #[must_use]
91    pub fn replaced_count(&self) -> usize {
92        self.replaced_end.saturating_sub(self.replaced_start)
93    }
94
95    /// Whether this artifact covers the given message index.
96    #[must_use]
97    pub fn covers_index(&self, idx: usize) -> bool {
98        idx >= self.replaced_start && idx < self.replaced_end
99    }
100}
101
102#[cfg(test)]
103mod tests {
104    use super::*;
105
106    #[test]
107    fn compaction_artifact_covers_index() {
108        let art = CompactionArtifact {
109            id: "test-id".to_string(),
110            session_id: "sess".to_string(),
111            created_at_ms: 0,
112            replaced_start: 2,
113            replaced_end: 5,
114            replaced_messages_json: "[]".to_string(),
115            summary: "summary".to_string(),
116            original_tokens: 100,
117            summary_tokens: 20,
118        };
119        assert!(!art.covers_index(1));
120        assert!(art.covers_index(2));
121        assert!(art.covers_index(4));
122        assert!(!art.covers_index(5));
123        assert_eq!(art.replaced_count(), 3);
124    }
125}