use anyhow::Result;
use crate::db;
use crate::db::project_from_cwd;
use super::format::format_header_datetime;
use super::host::{resolve_host_kind, resolve_profile};
use super::policy::{ContextPolicy, SectionKind};
use super::query::load_context_data_with_policy;
use super::sections::{
render_core_memory_with_limits, render_empty_state, render_memory_index_with_limits,
render_recent_sessions_with_limit, render_workstreams_with_limits,
};
use super::types::ContextRequest;
pub fn generate_context(
cwd: &str,
session_id: Option<&str>,
use_colors: bool,
host_arg: Option<&str>,
) -> Result<()> {
let timer = crate::log::Timer::start("context", &format!("cwd={}", cwd));
let project = project_from_cwd(cwd);
let current_branch = db::detect_git_branch(cwd);
let host = resolve_host_kind(host_arg);
let profile = resolve_profile(host);
let policy = profile.default_policy();
let request = ContextRequest {
cwd: cwd.to_string(),
project: project.clone(),
session_id: session_id.map(str::to_string),
current_branch: current_branch.clone(),
host: profile.host(),
use_colors,
};
let capabilities = profile.capabilities();
let conn = match db::open_db() {
Ok(connection) => connection,
Err(error) => {
crate::log::warn(
"context",
&format!("open_db failed for project={}: {}", project, error),
);
render_empty_state(&project);
timer.done("empty (no DB)");
return Ok(());
}
};
let loaded = load_context_data_with_policy(
&conn,
&request.project,
request.current_branch.as_deref(),
&policy,
);
let (preference_output, preference_count) =
match render_preferences_to_buffer(&conn, &project, cwd, &policy) {
Ok(rendered) => rendered,
Err(error) => {
crate::log::warn("context", &format!("render_preferences failed: {}", error));
(String::new(), 0)
}
};
if preference_count == 0
&& loaded.memories.is_empty()
&& loaded.summaries.is_empty()
&& loaded.workstreams.is_empty()
{
render_empty_state(&project);
timer.done("empty (no data)");
return Ok(());
}
let mut output = String::new();
output.push_str(&build_context_header(
&request.project,
request.current_branch.as_deref(),
));
output.push_str(profile.retrieval_hints().line);
output.push_str("\n\n");
output.push_str(&preference_output);
let mut core_count = 0;
let mut index_count = 0;
if !loaded.memories.is_empty() {
let render_limits = section_render_limits(&policy);
core_count = render_core_memory_with_limits(&mut output, &loaded.memories, &render_limits);
index_count =
render_memory_index_with_limits(&mut output, &loaded.memories, &render_limits);
}
if !loaded.workstreams.is_empty() {
render_workstreams_with_limits(
&mut output,
&loaded.workstreams,
policy.section_item_limit(SectionKind::Workstreams, 5),
policy.section_char_limit(SectionKind::Workstreams, 1_200),
);
}
if !loaded.summaries.is_empty() {
render_recent_sessions_with_limit(
&mut output,
&loaded.summaries,
policy.section_char_limit(SectionKind::Sessions, 2_200),
);
}
let stats_footer = format!(
"{} context memories loaded. {} core memories. {} indexed memories. {} preferences. {} sessions.\n",
loaded.memories.len(),
core_count,
index_count,
preference_count,
loaded.summaries.len()
);
output.push_str(&stats_footer);
enforce_total_char_limit_preserving_footer(
&mut output,
policy.limits.total_char_limit,
&stats_footer,
);
print!("{}", output);
timer.done(&format!(
"project={} cwd={} session={} host={} colors={} caps=[mcp:{} session_start:{} prompt_submit:{} native_edits:{} bash:{}] context_memories={} core={} index={} preferences={} summaries={} workstreams={}",
request.project,
request.cwd,
request.session_id.as_deref().unwrap_or("-"),
request.host.as_env_value(),
request.use_colors,
capabilities.has_mcp_tools,
capabilities.has_session_start_hook,
capabilities.has_user_prompt_submit_hook,
capabilities.observes_native_file_edits,
capabilities.observes_bash,
loaded.memories.len(),
core_count,
index_count,
preference_count,
loaded.summaries.len(),
loaded.workstreams.len(),
));
Ok(())
}
fn section_render_limits(policy: &ContextPolicy) -> super::policy::ContextLimits {
let mut limits = policy.limits;
limits.core_item_limit = policy.section_item_limit(SectionKind::Core, limits.core_item_limit);
limits.core_char_limit = policy.section_char_limit(SectionKind::Core, limits.core_char_limit);
limits.memory_index_limit =
policy.section_item_limit(SectionKind::MemoryIndex, limits.memory_index_limit);
limits.memory_index_char_limit =
policy.section_char_limit(SectionKind::MemoryIndex, limits.memory_index_char_limit);
limits
}
fn render_preferences_to_buffer(
conn: &rusqlite::Connection,
project: &str,
cwd: &str,
policy: &ContextPolicy,
) -> Result<(String, usize)> {
let mut output = String::new();
let limits = &policy.limits;
let count = crate::memory::preference::render_preferences_with_limits(
&mut output,
conn,
project,
cwd,
limits.preference_project_limit,
limits.preference_global_limit,
limits.preference_char_limit,
)?;
Ok((output, count))
}
#[cfg(test)]
pub(in crate::context) fn enforce_total_char_limit(output: &mut String, char_limit: usize) {
enforce_total_char_limit_preserving_footer(output, char_limit, "");
}
pub(in crate::context) fn enforce_total_char_limit_preserving_footer(
output: &mut String,
char_limit: usize,
footer: &str,
) {
if char_limit == 0 || output.chars().count() <= char_limit {
return;
}
let marker = "\n[remem context truncated to REMEM_CONTEXT_TOTAL_CHAR_LIMIT]\n";
let marker_chars = marker.chars().count();
let footer_chars = footer.chars().count();
if !footer.is_empty() && output.ends_with(footer) && marker_chars + footer_chars <= char_limit {
let keep_chars = char_limit - marker_chars - footer_chars;
let body = output.strip_suffix(footer).unwrap_or(output.as_str());
let mut truncated: String = body.chars().take(keep_chars).collect();
truncated.push_str(marker);
truncated.push_str(footer);
*output = truncated;
return;
}
if marker_chars >= char_limit {
*output = output.chars().take(char_limit).collect();
return;
}
let keep_chars = char_limit - marker_chars;
let mut truncated: String = output.chars().take(keep_chars).collect();
truncated.push_str(marker);
*output = truncated;
}
fn build_context_header(project: &str, current_branch: Option<&str>) -> String {
let branch_label = current_branch
.map(|branch| format!(" @{}", branch))
.unwrap_or_default();
format!(
"# [{}{}] context {}\n",
project,
branch_label,
format_header_datetime()
)
}