use anyhow::{bail, Result};
use rusqlite::Connection;
use crate::context_bundle::{ChannelKind, ContextItem, ItemValidity, SourceKind, TrustClass};
use crate::memory::{Memory, MemoryStalenessLabel, MemoryType};
use std::collections::HashSet;
use super::policy::ContextPolicy;
use super::query::load_context_data_with_policy;
use super::relevance::{memory_stable_key, session_stable_key};
use super::types::{LoadedContext, SessionSummaryBrief};
const STATUS_QUARANTINED: &str = "quarantined";
const STATUS_SUPERSEDED: &str = "superseded";
pub(crate) fn load_session_start_candidates(
conn: &Connection,
project: &str,
cwd: &str,
current_branch: Option<&str>,
) -> Result<Vec<ContextItem>> {
let policy = ContextPolicy::from_env();
let mut loaded = load_context_data_with_policy(conn, project, current_branch, &policy, false);
if !loaded.errors.is_empty() {
let sections: Vec<&str> = loaded.errors.iter().map(|error| error.section).collect();
bail!(
"canonical context load failed for sections [{}]: {}",
sections.join(", "),
loaded
.errors
.iter()
.map(|error| error.message.as_str())
.collect::<Vec<_>>()
.join("; ")
);
}
super::poisoning::drop_unacknowledged_poisoned_context(conn, &mut loaded);
let mut discarded_core = String::new();
let core = super::sections::render_core_memory_with_limits_and_staleness(
&mut discarded_core,
&loaded.memories,
&policy.limits,
loaded.render_reference_epoch,
&loaded.staleness_labels,
);
let core_ids = core.ids.into_iter().collect::<HashSet<_>>();
let mut items = candidates_from_loaded(&loaded, project, &core_ids);
items.extend(preference_candidates(conn, project, cwd, &policy)?);
Ok(items)
}
pub(super) fn session_start_candidates_from_loaded(
loaded: &LoadedContext,
project: &str,
preferences: &[Memory],
core_ids: &HashSet<i64>,
) -> Result<Vec<ContextItem>> {
if !loaded.errors.is_empty() {
bail!(
"canonical context load failed for sections [{}]",
loaded
.errors
.iter()
.map(|error| error.section)
.collect::<Vec<_>>()
.join(", ")
);
}
let mut items = candidates_from_loaded(loaded, project, core_ids);
items.extend(ordered_preference_candidates(preferences, project));
Ok(items)
}
fn preference_candidates(
conn: &Connection,
project: &str,
cwd: &str,
policy: &ContextPolicy,
) -> Result<Vec<ContextItem>> {
let mut discarded_render = String::new();
let limits = &policy.limits;
let details = crate::memory::preference::render_preferences_with_context_details(
&mut discarded_render,
conn,
project,
cwd,
limits.preference_project_limit,
limits.preference_global_limit,
limits.preference_char_limit,
)?;
Ok(ordered_preference_candidates(
&details.rendered_memories,
project,
))
}
fn ordered_preference_candidates(selected: &[Memory], project: &str) -> Vec<ContextItem> {
selected
.iter()
.map(|memory| {
let mut item = bundle_memory_item(memory, ChannelKind::Preferences, None, project);
item.branch = None;
item
})
.collect()
}
fn candidates_from_loaded(
loaded: &LoadedContext,
project: &str,
core_ids: &HashSet<i64>,
) -> Vec<ContextItem> {
let mut items = Vec::new();
for memory in loaded.memories.iter().filter(|memory| {
core_ids.contains(&memory.id)
|| MemoryType::parse(&memory.memory_type).is_none_or(MemoryType::is_indexed)
}) {
let label = loaded.staleness_labels.get(&memory.id);
items.push(bundle_memory_item(
memory,
memory_channel(memory, core_ids),
label,
project,
));
}
for lesson in &loaded.lessons {
let memory = &lesson.memory;
let label = loaded.staleness_labels.get(&memory.id);
items.push(bundle_memory_item(
memory,
ChannelKind::Lessons,
label,
project,
));
}
for workstream in &loaded.workstreams {
items.push(ContextItem {
stable_key: format!("workstream:{}", workstream.id),
channel: ChannelKind::Workstreams,
title: workstream.title.clone(),
text: workstream_text(workstream),
source_kind: SourceKind::Canonical,
canonical_ref: Some(format!("workstream:{}", workstream.id)),
projection_ref: None,
evidence_refs: Vec::new(),
validity: ItemValidity::Current,
trust: TrustClass::Standard,
project: Some(workstream.project.clone()),
branch: None,
});
}
for summary in &loaded.summaries {
items.push(summary_item(summary, project));
}
items
}
fn memory_channel(memory: &Memory, core_ids: &HashSet<i64>) -> ChannelKind {
if core_ids.contains(&memory.id) {
ChannelKind::Core
} else {
ChannelKind::MemoryIndex
}
}
fn bundle_memory_item(
memory: &Memory,
channel: ChannelKind,
label: Option<&MemoryStalenessLabel>,
project: &str,
) -> ContextItem {
ContextItem {
stable_key: memory_stable_key(memory.id),
channel,
title: memory.title.clone(),
text: memory.text.clone(),
source_kind: SourceKind::Canonical,
canonical_ref: Some(memory_stable_key(memory.id)),
projection_ref: None,
evidence_refs: Vec::new(),
validity: validity_for(memory, label),
trust: trust_for(memory),
project: Some(project.to_string()),
branch: memory.branch.clone(),
}
.with_project_fallback(project)
}
fn summary_item(summary: &SessionSummaryBrief, project: &str) -> ContextItem {
ContextItem {
stable_key: session_stable_key(summary.id),
channel: ChannelKind::Sessions,
title: summary.request.clone(),
text: summary.completed.clone().unwrap_or_default(),
source_kind: SourceKind::Canonical,
canonical_ref: Some(session_stable_key(summary.id)),
projection_ref: None,
evidence_refs: Vec::new(),
validity: ItemValidity::Current,
trust: TrustClass::Standard,
project: Some(project.to_string()),
branch: None,
}
}
fn validity_for(memory: &Memory, label: Option<&MemoryStalenessLabel>) -> ItemValidity {
if memory.status == STATUS_SUPERSEDED {
return ItemValidity::Superseded;
}
match label.map(|label| label.age) {
Some("old") | Some("aging") => ItemValidity::Stale,
_ => ItemValidity::Current,
}
}
fn trust_for(memory: &Memory) -> TrustClass {
if memory.status == STATUS_QUARANTINED {
TrustClass::Quarantined
} else {
TrustClass::Standard
}
}
fn workstream_text(workstream: &crate::workstream::WorkStream) -> String {
let mut text = workstream.title.clone();
if let Some(next_action) = &workstream.next_action {
text.push_str(" -> ");
text.push_str(next_action);
}
if let Some(blockers) = &workstream.blockers {
text.push_str(" (blockers: ");
text.push_str(blockers);
text.push(')');
}
text
}
impl ContextItem {
fn with_project_fallback(mut self, project: &str) -> Self {
if self
.project
.as_deref()
.is_none_or(|value| value.trim().is_empty())
{
self.project = Some(project.to_string());
}
self
}
}