feat: add ecc2 automatic graph relations

This commit is contained in:
Affaan Mustafa 2026-04-10 04:18:18 -07:00
parent 4adb3324ef
commit 315b87d391
2 changed files with 126 additions and 2 deletions

View File

@ -1262,6 +1262,7 @@ impl StateStore {
alternatives: &[String],
reasoning: &str,
) -> Result<()> {
let session_entity = self.sync_context_graph_session(session_id)?;
let mut metadata = BTreeMap::new();
metadata.insert(
"alternatives_count".to_string(),
@ -1270,7 +1271,7 @@ impl StateStore {
if !alternatives.is_empty() {
metadata.insert("alternatives".to_string(), alternatives.join(" | "));
}
self.upsert_context_entity(
let decision_entity = self.upsert_context_entity(
Some(session_id),
"decision",
decision,
@ -1278,6 +1279,14 @@ impl StateStore {
reasoning,
&metadata,
)?;
let relation_summary = format!("{} recorded this decision", session_entity.name);
self.upsert_context_relation(
Some(session_id),
session_entity.id,
decision_entity.id,
"decided",
&relation_summary,
)?;
Ok(())
}
@ -1287,6 +1296,7 @@ impl StateStore {
tool_name: &str,
event: &PersistedFileEvent,
) -> Result<()> {
let session_entity = self.sync_context_graph_session(session_id)?;
let mut metadata = BTreeMap::new();
metadata.insert(
"last_action".to_string(),
@ -1305,7 +1315,7 @@ impl StateStore {
format!("Last activity: {action} via {tool_name}")
};
let name = context_graph_file_name(&event.path);
self.upsert_context_entity(
let file_entity = self.upsert_context_entity(
Some(session_id),
"file",
&name,
@ -1313,9 +1323,57 @@ impl StateStore {
&summary,
&metadata,
)?;
self.upsert_context_relation(
Some(session_id),
session_entity.id,
file_entity.id,
action,
&summary,
)?;
Ok(())
}
fn sync_context_graph_session(&self, session_id: &str) -> Result<ContextGraphEntity> {
let session = self
.get_session(session_id)?
.ok_or_else(|| anyhow::anyhow!("Session not found for context graph sync: {session_id}"))?;
let mut metadata = BTreeMap::new();
metadata.insert("task".to_string(), session.task.clone());
metadata.insert("project".to_string(), session.project.clone());
metadata.insert("task_group".to_string(), session.task_group.clone());
metadata.insert("agent_type".to_string(), session.agent_type.clone());
metadata.insert("state".to_string(), session.state.to_string());
metadata.insert(
"working_dir".to_string(),
session.working_dir.display().to_string(),
);
if let Some(pid) = session.pid {
metadata.insert("pid".to_string(), pid.to_string());
}
if let Some(worktree) = &session.worktree {
metadata.insert(
"worktree_path".to_string(),
worktree.path.display().to_string(),
);
metadata.insert("worktree_branch".to_string(), worktree.branch.clone());
metadata.insert("base_branch".to_string(), worktree.base_branch.clone());
}
let summary = format!(
"{} | {} | {} / {}",
session.state, session.agent_type, session.project, session.task_group
);
self.upsert_context_entity(
Some(&session.id),
"session",
&session.id,
None,
&summary,
&metadata,
)
}
pub fn increment_tool_calls(&self, session_id: &str) -> Result<()> {
self.conn.execute(
"UPDATE sessions
@ -3832,6 +3890,20 @@ mod tests {
.summary
.contains("SQLite keeps the graph queryable"));
let session_entities = db.list_context_entities(Some("session-1"), Some("session"), 10)?;
assert_eq!(session_entities.len(), 1);
assert_eq!(session_entities[0].name, "session-1");
assert_eq!(
session_entities[0].metadata.get("task"),
Some(&"context graph".to_string())
);
let relations = db.list_context_relations(Some(session_entities[0].id), 10)?;
assert_eq!(relations.len(), 1);
assert_eq!(relations[0].relation_type, "decided");
assert_eq!(relations[0].to_entity_type, "decision");
assert_eq!(relations[0].to_entity_name, "Use sqlite for shared context");
Ok(())
}
@ -3883,6 +3955,14 @@ mod tests {
.summary
.contains("Last activity: modify via Edit"));
let session_entities = db.list_context_entities(Some("session-1"), Some("session"), 10)?;
assert_eq!(session_entities.len(), 1);
let relations = db.list_context_relations(Some(session_entities[0].id), 10)?;
assert_eq!(relations.len(), 1);
assert_eq!(relations[0].relation_type, "modify");
assert_eq!(relations[0].to_entity_type, "file");
assert_eq!(relations[0].to_entity_name, "config.ts");
Ok(())
}
@ -3953,6 +4033,14 @@ mod tests {
&& entity.name == "Backfill historical decision"));
assert!(entities.iter().any(|entity| entity.entity_type == "file"
&& entity.path.as_deref() == Some("src/backfill.rs")));
let session_entity = entities
.iter()
.find(|entity| entity.entity_type == "session" && entity.name == "session-1")
.expect("session entity should exist");
let relations = db.list_context_relations(Some(session_entity.id), 10)?;
assert_eq!(relations.len(), 2);
assert!(relations.iter().any(|relation| relation.relation_type == "decided"));
assert!(relations.iter().any(|relation| relation.relation_type == "modify"));
Ok(())
}

View File

@ -10104,12 +10104,14 @@ diff --git a/src/lib.rs b/src/lib.rs\n\
dashboard.toggle_context_graph_mode();
dashboard.toggle_search_scope();
dashboard.cycle_graph_entity_filter();
dashboard.begin_search();
for ch in "alpha.*".chars() {
dashboard.push_input_char(ch);
}
dashboard.submit_search();
assert_eq!(dashboard.graph_entity_filter, GraphEntityFilter::Decisions);
assert_eq!(dashboard.search_matches.len(), 2);
let first_session = dashboard.selected_session_id().map(str::to_string);
dashboard.next_search_match();
@ -10121,6 +10123,40 @@ diff --git a/src/lib.rs b/src/lib.rs\n\
Ok(())
}
#[test]
fn graph_sessions_filter_renders_auto_session_relations() -> Result<()> {
let session = sample_session(
"focus-12345678",
"planner",
SessionState::Running,
None,
1,
1,
);
let mut dashboard = test_dashboard(vec![session.clone()], 0);
dashboard.db.insert_session(&session)?;
dashboard.db.insert_decision(
&session.id,
"Use graph relations",
&[],
"Edges make the context graph navigable",
)?;
dashboard.toggle_context_graph_mode();
dashboard.cycle_graph_entity_filter();
dashboard.cycle_graph_entity_filter();
dashboard.cycle_graph_entity_filter();
dashboard.cycle_graph_entity_filter();
assert_eq!(dashboard.graph_entity_filter, GraphEntityFilter::Sessions);
assert_eq!(dashboard.output_title(), " Graph sessions ");
let rendered = dashboard.rendered_output_text(180, 30);
assert!(rendered.contains("focus-12345678"));
assert!(rendered.contains("summary running | planner |"));
assert!(rendered.contains("-> decided decision:Use graph relations"));
Ok(())
}
#[test]
fn worktree_diff_columns_split_removed_and_added_lines() {
let patch = "\