1use 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
35pub 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
109pub(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
149pub(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
182pub(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}