Skip to main content

remem/context_bundle/
compile.rs

1//! End-to-end SessionStart compile: request -> plan -> candidates ->
2//! bundle (GH-932).
3//!
4//! This is the first path that reads real data. Everything below the
5//! request is deterministic given the database contents: the plan is a
6//! pure function of the request, and the executor's selection is a pure
7//! function of the plan plus the candidate set.
8
9use anyhow::Result;
10use rusqlite::Connection;
11
12use crate::context::{
13    load_session_start_candidates_with_limits, ContextLimits, LoadedBundleCandidates,
14    SessionStartRelevancePlan,
15};
16use crate::retrieval::embedding::local_only_embedding_profile_fingerprint;
17use crate::retrieval_router::{
18    plan_context_bundle_with_limits, plan_session_start_with_limits, RetrievalPlan,
19};
20
21use super::current_truth::{
22    activate_current_truth_channel, annotate_current_truth_items, attach_shadow_comparison,
23};
24use super::domain::{ContextBundle, ContextItem, ContextRequest};
25use super::executor::{
26    blocked_before_load, execute, execute_with_trace, BudgetEnforcement, ExecutorInputs,
27};
28use crate::truth::CurrentTruthProjection;
29
30pub(crate) struct SessionStartCompile {
31    pub bundle: ContextBundle,
32    pub relevance_plan: SessionStartRelevancePlan,
33}
34
35/// Compile a SessionStart bundle from the database.
36///
37/// Returns `Err` only when the plan itself cannot be compiled (an invalid
38/// request). A canonical load failure is *not* an error return: it
39/// produces a `Blocked` bundle so the failure travels with the audit
40/// instead of being swallowed or mistaken for an empty project.
41pub fn compile_session_start_bundle(
42    conn: &Connection,
43    request: &ContextRequest,
44    cwd: &str,
45    current_branch: Option<&str>,
46    enrichment_available: bool,
47) -> Result<ContextBundle> {
48    let limits = ContextLimits::from_runtime()?;
49    let local_embedding_fingerprint = local_only_embedding_profile_fingerprint();
50    let compiled = plan_context_bundle_with_limits(request, &limits, &local_embedding_fingerprint)?;
51    Ok(bundle_for_plan(
52        conn,
53        &compiled,
54        &request.project.key,
55        cwd,
56        current_branch,
57        &limits,
58        enrichment_available,
59    ))
60}
61
62#[allow(clippy::too_many_arguments)]
63fn bundle_for_plan(
64    conn: &Connection,
65    compiled: &RetrievalPlan,
66    project: &str,
67    cwd: &str,
68    current_branch: Option<&str>,
69    limits: &ContextLimits,
70    enrichment_available: bool,
71) -> ContextBundle {
72    match load_session_start_candidates_with_limits(conn, project, cwd, current_branch, limits) {
73        Ok(LoadedBundleCandidates {
74            mut candidates,
75            poisoning_drops,
76            preselection_drops,
77            current_truth_projection,
78        }) => {
79            let Some(projection) = current_truth_projection else {
80                return blocked_before_load(compiled, "CurrentTruth projection unavailable");
81            };
82            annotate_current_truth_items(&mut candidates, &projection);
83            let mut bundle = execute(
84                compiled,
85                &ExecutorInputs {
86                    candidates,
87                    poisoning_drops,
88                    preselection_drops,
89                    enrichment_available,
90                },
91            );
92            attach_shadow_comparison(&mut bundle, &projection);
93            activate_current_truth_channel(
94                &mut bundle,
95                &projection,
96                Some(&compiled.section_budgets),
97                compiled
98                    .output_sections
99                    .iter()
100                    .find(|section| section.channel == super::domain::ChannelKind::Core)
101                    .map(|section| section.item_limit),
102            );
103            bundle
104        }
105        Err(error) => blocked_before_load(compiled, &error.to_string()),
106    }
107}
108
109/// Compile the bundle consumed by the production SessionStart compatibility
110/// renderer from the caller's single canonical snapshot.
111///
112/// Scope, trust, and relevance decisions are final here. Exact item/character
113/// budgets are deliberately deferred to the established renderer so adopting
114/// the bundle cannot change a byte of host-visible context. The renderer must
115/// call [`seal_session_start_bundle`] before the bundle can leave that path.
116pub(crate) fn compile_session_start_for_renderer(
117    request: &ContextRequest,
118    limits: &ContextLimits,
119    mut candidates: Vec<ContextItem>,
120    poisoning_drops: Vec<ContextItem>,
121    preselection_drops: Vec<super::executor::PreselectionDrop>,
122    enrichment_available: bool,
123    current_truth: Option<&CurrentTruthProjection>,
124) -> Result<SessionStartCompile> {
125    if let Some(projection) = current_truth {
126        annotate_current_truth_items(&mut candidates, projection);
127    }
128    let compiled = plan_session_start_with_limits(request, limits)?;
129    let mut trace = execute_with_trace(
130        &compiled,
131        &ExecutorInputs {
132            candidates,
133            poisoning_drops,
134            preselection_drops,
135            enrichment_available,
136        },
137        BudgetEnforcement::DeferToRenderer,
138    );
139    if let Some(projection) = current_truth {
140        attach_shadow_comparison(&mut trace.bundle, projection);
141        activate_current_truth_channel(&mut trace.bundle, projection, None, None);
142    }
143    Ok(SessionStartCompile {
144        bundle: trace.bundle,
145        relevance_plan: trace.relevance_plan,
146    })
147}
148
149/// Seal a renderer-deferred bundle to the exact identities that survived
150/// section and total character budgets.
151pub(crate) fn seal_session_start_bundle(
152    bundle: &mut ContextBundle,
153    selected_keys: &std::collections::HashSet<String>,
154    total_truncated_keys: &std::collections::HashSet<String>,
155    output_chars: usize,
156) {
157    retain_selected_sections(bundle, selected_keys);
158    for entry in &mut bundle.audit.entries {
159        if entry.selected && !selected_keys.contains(&entry.stable_key) {
160            entry.selected = false;
161            entry.reason = if total_truncated_keys.contains(&entry.stable_key) {
162                "total_char_limit"
163            } else {
164                "section_budget"
165            }
166            .to_string();
167        }
168    }
169    bundle.audit.selected_count = bundle
170        .audit
171        .entries
172        .iter()
173        .filter(|entry| entry.selected)
174        .count() as u32;
175    bundle.audit.dropped_count = bundle.audit.candidates_considered - bundle.audit.selected_count;
176    bundle.audit.token_estimate = (output_chars as u32).div_ceil(4);
177    if !total_truncated_keys.is_empty() {
178        bundle.audit.truncation_reason = Some("total_char_limit".to_string());
179    }
180}
181
182/// Reseal a rendered bundle to the identities that survived a later output
183/// gate, so the durable audit describes the bytes actually emitted.
184pub(crate) fn reseal_after_emission_gate(
185    bundle: &mut ContextBundle,
186    selected_keys: &std::collections::HashSet<String>,
187    output_chars: usize,
188    drop_reason: &str,
189    output_truncated: bool,
190) {
191    retain_selected_sections(bundle, selected_keys);
192    let mut dropped_by_gate = false;
193    for entry in &mut bundle.audit.entries {
194        if entry.selected && !selected_keys.contains(&entry.stable_key) {
195            entry.selected = false;
196            entry.reason = drop_reason.to_string();
197            dropped_by_gate = true;
198        }
199    }
200    bundle.audit.selected_count = bundle
201        .audit
202        .entries
203        .iter()
204        .filter(|entry| entry.selected)
205        .count() as u32;
206    bundle.audit.dropped_count = bundle.audit.candidates_considered - bundle.audit.selected_count;
207    bundle.audit.token_estimate = (output_chars as u32).div_ceil(4);
208    if dropped_by_gate || output_truncated {
209        bundle.audit.truncation_reason = Some(drop_reason.to_string());
210    }
211}
212
213fn retain_selected_sections(
214    bundle: &mut ContextBundle,
215    selected_keys: &std::collections::HashSet<String>,
216) {
217    for section in [
218        &mut bundle.preferences,
219        &mut bundle.failure_lessons,
220        &mut bundle.current_truth,
221        &mut bundle.workstreams,
222        &mut bundle.memory_index,
223        &mut bundle.recent_sessions,
224    ] {
225        section.retain(|item| selected_keys.contains(&item.stable_key));
226    }
227}