use anyhow::{bail, Result};
use rusqlite::{params, Connection};
use super::{parse_memory_ref_id, ParsedGraphCandidate};
use crate::memory::conflict_common::{common_optional_i64, common_optional_string, common_string};
use crate::memory::edge::{insert_pairwise_conflict_edges, MemoryEdgeWriteContext};
use crate::memory::operation::MemoryOperationInput;
#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) struct MemoryConflictBridge {
pub(super) ids: Vec<i64>,
pub(super) owner_scope: String,
pub(super) owner_key: String,
pub(super) memory_type: String,
pub(super) topic_key: Option<String>,
pub(super) state_key: Option<String>,
pub(super) state_key_id: Option<i64>,
}
impl MemoryConflictBridge {
pub(super) fn operation_input(
&self,
source_project: &str,
candidate_id: i64,
candidate: &ParsedGraphCandidate,
actor: &str,
) -> MemoryOperationInput {
MemoryOperationInput {
source: "graph_candidate".to_string(),
actor: actor.to_string(),
source_project: source_project.to_string(),
owner_scope: self.owner_scope.clone(),
owner_key: self.owner_key.clone(),
memory_type: self.memory_type.clone(),
topic_key: self.topic_key.clone().or(Some(candidate.edge_type.clone())),
state_key: self.state_key.clone(),
source_candidate_id: Some(candidate_id),
confidence: Some(candidate.confidence),
}
}
pub(super) fn insert_memory_edges(
&self,
conn: &Connection,
operation_id: i64,
candidate: &ParsedGraphCandidate,
) -> Result<usize> {
insert_pairwise_conflict_edges(
conn,
&self.ids,
MemoryEdgeWriteContext {
state_key_id: self.state_key_id,
source_candidate_id: None,
evidence_event_ids: &candidate.evidence_event_ids,
source_operation_id: Some(operation_id),
confidence: Some(candidate.confidence),
reason: Some(candidate.reason.as_str()),
},
)
}
}
pub(super) fn build_memory_conflict_bridge(
conn: &Connection,
source_project: &str,
candidate: &ParsedGraphCandidate,
prompt_memory_ref_ids: Option<&[i64]>,
) -> Result<Option<MemoryConflictBridge>> {
if candidate.edge_type != "conflicts" {
return Ok(None);
}
let Some(from_id) = parse_memory_ref_id(&candidate.from_ref)? else {
bail!("graph conflict candidates must use memory:* endpoints");
};
let Some(to_id) = parse_memory_ref_id(&candidate.to_ref)? else {
bail!("graph conflict candidates must use memory:* endpoints");
};
if from_id == to_id {
bail!("graph conflict candidate cannot point to the same memory twice");
}
let mut ids = vec![from_id, to_id];
ids.sort_unstable();
ids.dedup();
let Some(prompt_memory_ref_ids) = prompt_memory_ref_ids else {
bail!("graph conflict candidates require persisted prompt memory_refs");
};
if !ids
.iter()
.all(|memory_id| prompt_memory_ref_ids.contains(memory_id))
{
bail!("graph conflict candidate references memory outside provided memory_refs");
}
let rows = load_memory_ref_rows(conn, source_project, &ids)?;
if rows.len() != ids.len() {
bail!("graph conflict candidate references memory outside the active repo scope");
}
Ok(Some(MemoryConflictBridge {
ids,
owner_scope: common_string(rows.iter().map(|row| row.owner_scope.as_str()))
.unwrap_or_else(|| "repo".to_string()),
owner_key: common_string(rows.iter().map(|row| row.owner_key.as_str()))
.unwrap_or_else(|| source_project.to_string()),
memory_type: common_string(rows.iter().map(|row| row.memory_type.as_str()))
.unwrap_or_else(|| "memory".to_string()),
topic_key: common_optional_string(rows.iter().map(|row| row.topic_key.as_deref())),
state_key: common_optional_string(rows.iter().map(|row| row.state_key.as_deref())),
state_key_id: common_optional_i64(rows.iter().map(|row| row.state_key_id)),
}))
}
#[derive(Debug)]
struct MemoryRefRow {
owner_scope: String,
owner_key: String,
memory_type: String,
topic_key: Option<String>,
state_key: Option<String>,
state_key_id: Option<i64>,
}
fn load_memory_ref_rows(
conn: &Connection,
source_project: &str,
memory_ids: &[i64],
) -> Result<Vec<MemoryRefRow>> {
let mut rows = Vec::with_capacity(memory_ids.len());
let current_filter =
crate::memory::memory_current_filter_sql("m.status", "m.expires_at_epoch", false);
for memory_id in memory_ids {
rows.push(conn.query_row(
&format!(
"SELECT
COALESCE(
m.owner_scope,
CASE WHEN COALESCE(m.scope, 'project') = 'global' THEN 'user' ELSE 'repo' END
) AS owner_scope,
COALESCE(
m.owner_key,
CASE WHEN COALESCE(m.scope, 'project') = 'global' THEN 'user:default' ELSE m.project END
) AS owner_key,
m.memory_type,
m.topic_key,
sk.state_key,
m.state_key_id
FROM memories m
LEFT JOIN memory_state_keys sk ON sk.id = m.state_key_id
WHERE m.id = ?1
AND {current_filter}
AND (
(m.owner_scope = 'repo' AND m.owner_key = ?2)
OR m.target_project = ?2
OR (
m.owner_scope IS NULL
AND m.project = ?2
AND COALESCE(m.scope, 'project') != 'global'
)
)
LIMIT 1"
),
params![memory_id, source_project],
|row| {
Ok(MemoryRefRow {
owner_scope: row.get(0)?,
owner_key: row.get(1)?,
memory_type: row.get(2)?,
topic_key: row.get(3)?,
state_key: row.get(4)?,
state_key_id: row.get(5)?,
})
},
)?);
}
Ok(rows)
}