feat: add ecc2 memory observation priorities

This commit is contained in:
Affaan Mustafa
2026-04-10 06:56:26 -07:00
parent 9523575721
commit 766bf31737
4 changed files with 181 additions and 29 deletions

View File

@@ -470,6 +470,9 @@ enum GraphCommands {
/// Observation type such as completion_summary, incident_note, or reminder /// Observation type such as completion_summary, incident_note, or reminder
#[arg(long = "type")] #[arg(long = "type")]
observation_type: String, observation_type: String,
/// Observation priority
#[arg(long, value_enum, default_value_t = ObservationPriorityArg::Normal)]
priority: ObservationPriorityArg,
/// Observation summary /// Observation summary
#[arg(long)] #[arg(long)]
summary: String, summary: String,
@@ -569,6 +572,25 @@ enum MessageKindArg {
Conflict, Conflict,
} }
#[derive(clap::ValueEnum, Clone, Debug)]
enum ObservationPriorityArg {
Low,
Normal,
High,
Critical,
}
impl From<ObservationPriorityArg> for session::ContextObservationPriority {
fn from(value: ObservationPriorityArg) -> Self {
match value {
ObservationPriorityArg::Low => Self::Low,
ObservationPriorityArg::Normal => Self::Normal,
ObservationPriorityArg::High => Self::High,
ObservationPriorityArg::Critical => Self::Critical,
}
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)] #[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
struct GraphConnectorSyncStats { struct GraphConnectorSyncStats {
connector_name: String, connector_name: String,
@@ -1365,6 +1387,7 @@ async fn main() -> Result<()> {
session_id, session_id,
entity_id, entity_id,
observation_type, observation_type,
priority,
summary, summary,
details, details,
json, json,
@@ -1378,6 +1401,7 @@ async fn main() -> Result<()> {
resolved_session_id.as_deref(), resolved_session_id.as_deref(),
entity_id, entity_id,
&observation_type, &observation_type,
priority.into(),
&summary, &summary,
&details, &details,
)?; )?;
@@ -2119,6 +2143,7 @@ fn import_memory_connector_record(
session_id.as_deref(), session_id.as_deref(),
entity.id, entity.id,
observation_type, observation_type,
session::ContextObservationPriority::Normal,
summary, summary,
&record.details, &record.details,
)?; )?;
@@ -3323,6 +3348,7 @@ fn format_graph_observation_human(observation: &session::ContextGraphObservation
observation.entity_id, observation.entity_type, observation.entity_name observation.entity_id, observation.entity_type, observation.entity_name
), ),
format!("Type: {}", observation.observation_type), format!("Type: {}", observation.observation_type),
format!("Priority: {}", observation.priority),
format!("Summary: {}", observation.summary), format!("Summary: {}", observation.summary),
]; ];
if let Some(session_id) = observation.session_id.as_deref() { if let Some(session_id) = observation.session_id.as_deref() {
@@ -3354,8 +3380,11 @@ fn format_graph_observations_human(observations: &[session::ContextGraphObservat
)]; )];
for observation in observations { for observation in observations {
let mut line = format!( let mut line = format!(
"- #{} [{}] {}", "- #{} [{}/{}] {}",
observation.id, observation.observation_type, observation.entity_name observation.id,
observation.observation_type,
observation.priority,
observation.entity_name
); );
if let Some(session_id) = observation.session_id.as_deref() { if let Some(session_id) = observation.session_id.as_deref() {
line.push_str(&format!(" | {}", short_session(session_id))); line.push_str(&format!(" | {}", short_session(session_id)));
@@ -3386,13 +3415,14 @@ fn format_graph_recall_human(
)]; )];
for entry in entries { for entry in entries {
let mut line = format!( let mut line = format!(
"- #{} [{}] {} | score {} | relations {} | observations {}", "- #{} [{}] {} | score {} | relations {} | observations {} | priority {}",
entry.entity.id, entry.entity.id,
entry.entity.entity_type, entry.entity.entity_type,
entry.entity.name, entry.entity.name,
entry.score, entry.score,
entry.relation_count, entry.relation_count,
entry.observation_count entry.observation_count,
entry.max_observation_priority
); );
if let Some(session_id) = entry.entity.session_id.as_deref() { if let Some(session_id) = entry.entity.session_id.as_deref() {
line.push_str(&format!(" | {}", short_session(session_id))); line.push_str(&format!(" | {}", short_session(session_id)));
@@ -5448,6 +5478,7 @@ mod tests {
session_id, session_id,
entity_id, entity_id,
observation_type, observation_type,
priority,
summary, summary,
details, details,
json, json,
@@ -5456,6 +5487,7 @@ mod tests {
assert_eq!(session_id.as_deref(), Some("latest")); assert_eq!(session_id.as_deref(), Some("latest"));
assert_eq!(entity_id, 7); assert_eq!(entity_id, 7);
assert_eq!(observation_type, "completion_summary"); assert_eq!(observation_type, "completion_summary");
assert!(matches!(priority, ObservationPriorityArg::Normal));
assert_eq!(summary, "Finished auth callback recovery"); assert_eq!(summary, "Finished auth callback recovery");
assert_eq!(details, vec!["tests_run=2"]); assert_eq!(details, vec!["tests_run=2"]);
assert!(json); assert!(json);
@@ -5668,6 +5700,7 @@ mod tests {
], ],
relation_count: 2, relation_count: 2,
observation_count: 1, observation_count: 1,
max_observation_priority: session::ContextObservationPriority::High,
}], }],
Some("sess-12345678"), Some("sess-12345678"),
"auth callback recovery", "auth callback recovery",
@@ -5675,6 +5708,7 @@ mod tests {
assert!(text.contains("Relevant memory: 1 entries")); assert!(text.contains("Relevant memory: 1 entries"));
assert!(text.contains("[file] callback.ts | score 319 | relations 2 | observations 1")); assert!(text.contains("[file] callback.ts | score 319 | relations 2 | observations 1"));
assert!(text.contains("priority high"));
assert!(text.contains("matches auth, callback, recovery")); assert!(text.contains("matches auth, callback, recovery"));
assert!(text.contains("path src/routes/auth/callback.ts")); assert!(text.contains("path src/routes/auth/callback.ts"));
} }
@@ -5688,6 +5722,7 @@ mod tests {
entity_type: "session".to_string(), entity_type: "session".to_string(),
entity_name: "sess-12345678".to_string(), entity_name: "sess-12345678".to_string(),
observation_type: "completion_summary".to_string(), observation_type: "completion_summary".to_string(),
priority: session::ContextObservationPriority::High,
summary: "Finished auth callback recovery with 2 tests".to_string(), summary: "Finished auth callback recovery with 2 tests".to_string(),
details: BTreeMap::from([("tests_run".to_string(), "2".to_string())]), details: BTreeMap::from([("tests_run".to_string(), "2".to_string())]),
created_at: chrono::DateTime::parse_from_rfc3339("2026-04-10T01:02:03Z") created_at: chrono::DateTime::parse_from_rfc3339("2026-04-10T01:02:03Z")
@@ -5696,7 +5731,7 @@ mod tests {
}]); }]);
assert!(text.contains("Context graph observations: 1")); assert!(text.contains("Context graph observations: 1"));
assert!(text.contains("[completion_summary] sess-12345678")); assert!(text.contains("[completion_summary/high] sess-12345678"));
assert!(text.contains("summary Finished auth callback recovery with 2 tests")); assert!(text.contains("summary Finished auth callback recovery with 2 tests"));
} }

View File

@@ -198,6 +198,7 @@ pub struct ContextGraphObservation {
pub entity_type: String, pub entity_type: String,
pub entity_name: String, pub entity_name: String,
pub observation_type: String, pub observation_type: String,
pub priority: ContextObservationPriority,
pub summary: String, pub summary: String,
pub details: BTreeMap<String, String>, pub details: BTreeMap<String, String>,
pub created_at: DateTime<Utc>, pub created_at: DateTime<Utc>,
@@ -210,6 +211,53 @@ pub struct ContextGraphRecallEntry {
pub matched_terms: Vec<String>, pub matched_terms: Vec<String>,
pub relation_count: usize, pub relation_count: usize,
pub observation_count: usize, pub observation_count: usize,
pub max_observation_priority: ContextObservationPriority,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
#[serde(rename_all = "snake_case")]
pub enum ContextObservationPriority {
Low,
Normal,
High,
Critical,
}
impl Default for ContextObservationPriority {
fn default() -> Self {
Self::Normal
}
}
impl fmt::Display for ContextObservationPriority {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Low => write!(f, "low"),
Self::Normal => write!(f, "normal"),
Self::High => write!(f, "high"),
Self::Critical => write!(f, "critical"),
}
}
}
impl ContextObservationPriority {
pub fn from_db_value(value: i64) -> Self {
match value {
0 => Self::Low,
2 => Self::High,
3 => Self::Critical,
_ => Self::Normal,
}
}
pub fn as_db_value(self) -> i64 {
match self {
Self::Low => 0,
Self::Normal => 1,
Self::High => 2,
Self::Critical => 3,
}
}
} }
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)] #[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]

View File

@@ -16,8 +16,8 @@ use super::{
default_project_label, default_task_group_label, normalize_group_label, default_project_label, default_task_group_label, normalize_group_label,
ContextGraphCompactionStats, ContextGraphEntity, ContextGraphEntityDetail, ContextGraphCompactionStats, ContextGraphEntity, ContextGraphEntityDetail,
ContextGraphObservation, ContextGraphRecallEntry, ContextGraphRelation, ContextGraphSyncStats, ContextGraphObservation, ContextGraphRecallEntry, ContextGraphRelation, ContextGraphSyncStats,
DecisionLogEntry, FileActivityAction, FileActivityEntry, Session, SessionAgentProfile, ContextObservationPriority, DecisionLogEntry, FileActivityAction, FileActivityEntry, Session,
SessionMessage, SessionMetrics, SessionState, WorktreeInfo, SessionAgentProfile, SessionMessage, SessionMetrics, SessionState, WorktreeInfo,
}; };
pub struct StateStore { pub struct StateStore {
@@ -267,6 +267,7 @@ impl StateStore {
session_id TEXT REFERENCES sessions(id) ON DELETE SET NULL, session_id TEXT REFERENCES sessions(id) ON DELETE SET NULL,
entity_id INTEGER NOT NULL REFERENCES context_graph_entities(id) ON DELETE CASCADE, entity_id INTEGER NOT NULL REFERENCES context_graph_entities(id) ON DELETE CASCADE,
observation_type TEXT NOT NULL, observation_type TEXT NOT NULL,
priority INTEGER NOT NULL DEFAULT 1,
summary TEXT NOT NULL, summary TEXT NOT NULL,
details_json TEXT NOT NULL DEFAULT '{}', details_json TEXT NOT NULL DEFAULT '{}',
created_at TEXT NOT NULL created_at TEXT NOT NULL
@@ -464,6 +465,15 @@ impl StateStore {
.context("Failed to add trigger_summary column to tool_log table")?; .context("Failed to add trigger_summary column to tool_log table")?;
} }
if !self.has_column("context_graph_observations", "priority")? {
self.conn
.execute(
"ALTER TABLE context_graph_observations ADD COLUMN priority INTEGER NOT NULL DEFAULT 1",
[],
)
.context("Failed to add priority column to context_graph_observations table")?;
}
if !self.has_column("daemon_activity", "last_dispatch_deferred")? { if !self.has_column("daemon_activity", "last_dispatch_deferred")? {
self.conn self.conn
.execute( .execute(
@@ -2088,6 +2098,12 @@ impl StateStore {
FROM context_graph_observations o FROM context_graph_observations o
WHERE o.entity_id = e.id WHERE o.entity_id = e.id
) AS observation_count ) AS observation_count
,
COALESCE((
SELECT MAX(priority)
FROM context_graph_observations o
WHERE o.entity_id = e.id
), 1) AS max_observation_priority
FROM context_graph_entities e FROM context_graph_entities e
WHERE (?1 IS NULL OR e.session_id = ?1) WHERE (?1 IS NULL OR e.session_id = ?1)
ORDER BY e.updated_at DESC, e.id DESC ORDER BY e.updated_at DESC, e.id DESC
@@ -2102,7 +2118,15 @@ impl StateStore {
let relation_count = row.get::<_, i64>(9)?.max(0) as usize; let relation_count = row.get::<_, i64>(9)?.max(0) as usize;
let observation_text = row.get::<_, String>(10)?; let observation_text = row.get::<_, String>(10)?;
let observation_count = row.get::<_, i64>(11)?.max(0) as usize; let observation_count = row.get::<_, i64>(11)?.max(0) as usize;
Ok((entity, relation_count, observation_text, observation_count)) let max_observation_priority =
ContextObservationPriority::from_db_value(row.get::<_, i64>(12)?);
Ok((
entity,
relation_count,
observation_text,
observation_count,
max_observation_priority,
))
}, },
)? )?
.collect::<Result<Vec<_>, _>>()?; .collect::<Result<Vec<_>, _>>()?;
@@ -2111,7 +2135,13 @@ impl StateStore {
let mut entries = candidates let mut entries = candidates
.into_iter() .into_iter()
.filter_map( .filter_map(
|(entity, relation_count, observation_text, observation_count)| { |(
entity,
relation_count,
observation_text,
observation_count,
max_observation_priority,
)| {
let matched_terms = let matched_terms =
context_graph_matched_terms(&entity, &observation_text, &terms); context_graph_matched_terms(&entity, &observation_text, &terms);
if matched_terms.is_empty() { if matched_terms.is_empty() {
@@ -2123,6 +2153,7 @@ impl StateStore {
matched_terms.len(), matched_terms.len(),
relation_count, relation_count,
observation_count, observation_count,
max_observation_priority,
entity.updated_at, entity.updated_at,
now, now,
), ),
@@ -2130,6 +2161,7 @@ impl StateStore {
matched_terms, matched_terms,
relation_count, relation_count,
observation_count, observation_count,
max_observation_priority,
}) })
}, },
) )
@@ -2217,6 +2249,7 @@ impl StateStore {
session_id: Option<&str>, session_id: Option<&str>,
entity_id: i64, entity_id: i64,
observation_type: &str, observation_type: &str,
priority: ContextObservationPriority,
summary: &str, summary: &str,
details: &BTreeMap<String, String>, details: &BTreeMap<String, String>,
) -> Result<ContextGraphObservation> { ) -> Result<ContextGraphObservation> {
@@ -2235,12 +2268,13 @@ impl StateStore {
let details_json = serde_json::to_string(details)?; let details_json = serde_json::to_string(details)?;
self.conn.execute( self.conn.execute(
"INSERT INTO context_graph_observations ( "INSERT INTO context_graph_observations (
session_id, entity_id, observation_type, summary, details_json, created_at session_id, entity_id, observation_type, priority, summary, details_json, created_at
) VALUES (?1, ?2, ?3, ?4, ?5, ?6)", ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
rusqlite::params![ rusqlite::params![
session_id, session_id,
entity_id, entity_id,
observation_type.trim(), observation_type.trim(),
priority.as_db_value(),
summary.trim(), summary.trim(),
details_json, details_json,
now, now,
@@ -2255,7 +2289,7 @@ impl StateStore {
self.conn self.conn
.query_row( .query_row(
"SELECT o.id, o.session_id, o.entity_id, e.entity_type, e.name, "SELECT o.id, o.session_id, o.entity_id, e.entity_type, e.name,
o.observation_type, o.summary, o.details_json, o.created_at o.observation_type, o.priority, o.summary, o.details_json, o.created_at
FROM context_graph_observations o FROM context_graph_observations o
JOIN context_graph_entities e ON e.id = o.entity_id JOIN context_graph_entities e ON e.id = o.entity_id
WHERE o.id = ?1", WHERE o.id = ?1",
@@ -2277,6 +2311,7 @@ impl StateStore {
&self, &self,
session_id: &str, session_id: &str,
observation_type: &str, observation_type: &str,
priority: ContextObservationPriority,
summary: &str, summary: &str,
details: &BTreeMap<String, String>, details: &BTreeMap<String, String>,
) -> Result<ContextGraphObservation> { ) -> Result<ContextGraphObservation> {
@@ -2285,6 +2320,7 @@ impl StateStore {
Some(session_id), Some(session_id),
session_entity.id, session_entity.id,
observation_type, observation_type,
priority,
summary, summary,
details, details,
) )
@@ -2297,7 +2333,7 @@ impl StateStore {
) -> Result<Vec<ContextGraphObservation>> { ) -> Result<Vec<ContextGraphObservation>> {
let mut stmt = self.conn.prepare( let mut stmt = self.conn.prepare(
"SELECT o.id, o.session_id, o.entity_id, e.entity_type, e.name, "SELECT o.id, o.session_id, o.entity_id, e.entity_type, e.name,
o.observation_type, o.summary, o.details_json, o.created_at o.observation_type, o.priority, o.summary, o.details_json, o.created_at
FROM context_graph_observations o FROM context_graph_observations o
JOIN context_graph_entities e ON e.id = o.entity_id JOIN context_graph_entities e ON e.id = o.entity_id
WHERE (?1 IS NULL OR o.entity_id = ?1) WHERE (?1 IS NULL OR o.entity_id = ?1)
@@ -3428,12 +3464,12 @@ fn map_context_graph_observation(
row: &rusqlite::Row<'_>, row: &rusqlite::Row<'_>,
) -> rusqlite::Result<ContextGraphObservation> { ) -> rusqlite::Result<ContextGraphObservation> {
let details_json = row let details_json = row
.get::<_, Option<String>>(7)? .get::<_, Option<String>>(8)?
.unwrap_or_else(|| "{}".to_string()); .unwrap_or_else(|| "{}".to_string());
let details = serde_json::from_str(&details_json).map_err(|error| { let details = serde_json::from_str(&details_json).map_err(|error| {
rusqlite::Error::FromSqlConversionFailure(7, rusqlite::types::Type::Text, Box::new(error)) rusqlite::Error::FromSqlConversionFailure(8, rusqlite::types::Type::Text, Box::new(error))
})?; })?;
let created_at = parse_store_timestamp(row.get::<_, String>(8)?, 8)?; let created_at = parse_store_timestamp(row.get::<_, String>(9)?, 9)?;
Ok(ContextGraphObservation { Ok(ContextGraphObservation {
id: row.get(0)?, id: row.get(0)?,
@@ -3442,7 +3478,8 @@ fn map_context_graph_observation(
entity_type: row.get(3)?, entity_type: row.get(3)?,
entity_name: row.get(4)?, entity_name: row.get(4)?,
observation_type: row.get(5)?, observation_type: row.get(5)?,
summary: row.get(6)?, priority: ContextObservationPriority::from_db_value(row.get::<_, i64>(6)?),
summary: row.get(7)?,
details, details,
created_at, created_at,
}) })
@@ -3496,6 +3533,7 @@ fn context_graph_recall_score(
matched_term_count: usize, matched_term_count: usize,
relation_count: usize, relation_count: usize,
observation_count: usize, observation_count: usize,
max_observation_priority: ContextObservationPriority,
updated_at: chrono::DateTime<chrono::Utc>, updated_at: chrono::DateTime<chrono::Utc>,
now: chrono::DateTime<chrono::Utc>, now: chrono::DateTime<chrono::Utc>,
) -> u64 { ) -> u64 {
@@ -3515,6 +3553,7 @@ fn context_graph_recall_score(
(matched_term_count as u64 * 100) (matched_term_count as u64 * 100)
+ (relation_count.min(9) as u64 * 10) + (relation_count.min(9) as u64 * 10)
+ (observation_count.min(6) as u64 * 8) + (observation_count.min(6) as u64 * 8)
+ (max_observation_priority.as_db_value() as u64 * 18)
+ recency_bonus + recency_bonus
} }
@@ -4336,6 +4375,7 @@ mod tests {
Some("session-1"), Some("session-1"),
entity.id, entity.id,
"note", "note",
ContextObservationPriority::Normal,
"Customer wiped setup and got charged twice", "Customer wiped setup and got charged twice",
&BTreeMap::from([("customer".to_string(), "viktor".to_string())]), &BTreeMap::from([("customer".to_string(), "viktor".to_string())]),
)?; )?;
@@ -4345,6 +4385,7 @@ mod tests {
assert_eq!(observations[0].id, observation.id); assert_eq!(observations[0].id, observation.id);
assert_eq!(observations[0].entity_name, "Prefer recovery-first routing"); assert_eq!(observations[0].entity_name, "Prefer recovery-first routing");
assert_eq!(observations[0].observation_type, "note"); assert_eq!(observations[0].observation_type, "note");
assert_eq!(observations[0].priority, ContextObservationPriority::Normal);
assert_eq!( assert_eq!(
observations[0].details.get("customer"), observations[0].details.get("customer"),
Some(&"viktor".to_string()) Some(&"viktor".to_string())
@@ -4393,12 +4434,13 @@ mod tests {
] { ] {
db.conn.execute( db.conn.execute(
"INSERT INTO context_graph_observations ( "INSERT INTO context_graph_observations (
session_id, entity_id, observation_type, summary, details_json, created_at session_id, entity_id, observation_type, priority, summary, details_json, created_at
) VALUES (?1, ?2, ?3, ?4, ?5, ?6)", ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
rusqlite::params![ rusqlite::params![
"session-1", "session-1",
entity.id, entity.id,
"note", "note",
ContextObservationPriority::Normal.as_db_value(),
summary, summary,
"{}", "{}",
chrono::Utc::now().to_rfc3339(), chrono::Utc::now().to_rfc3339(),
@@ -4460,6 +4502,7 @@ mod tests {
Some("session-1"), Some("session-1"),
entity.id, entity.id,
"completion_summary", "completion_summary",
ContextObservationPriority::Normal,
&summary, &summary,
&BTreeMap::new(), &BTreeMap::new(),
)?; )?;
@@ -4542,6 +4585,7 @@ mod tests {
Some("session-1"), Some("session-1"),
recovery.id, recovery.id,
"incident_note", "incident_note",
ContextObservationPriority::High,
"Previous auth callback recovery incident affected Viktor after a wipe", "Previous auth callback recovery incident affected Viktor after a wipe",
&BTreeMap::new(), &BTreeMap::new(),
)?; )?;
@@ -4550,19 +4594,32 @@ mod tests {
db.recall_context_entities(Some("session-1"), "Investigate auth callback recovery", 3)?; db.recall_context_entities(Some("session-1"), "Investigate auth callback recovery", 3)?;
assert_eq!(results.len(), 2); assert_eq!(results.len(), 2);
assert_eq!(results[0].entity.id, callback.id); assert_eq!(results[0].entity.id, recovery.id);
assert!(results[0].matched_terms.iter().any(|term| term == "auth")); assert!(results[0].matched_terms.iter().any(|term| term == "auth"));
assert!(results[0] assert!(results[0]
.matched_terms
.iter()
.any(|term| term == "recovery"));
assert_eq!(results[0].observation_count, 1);
assert_eq!(
results[0].max_observation_priority,
ContextObservationPriority::High
);
assert_eq!(results[1].entity.id, callback.id);
assert!(results[1]
.matched_terms .matched_terms
.iter() .iter()
.any(|term| term == "callback")); .any(|term| term == "callback"));
assert!(results[0] assert!(results[1]
.matched_terms .matched_terms
.iter() .iter()
.any(|term| term == "recovery")); .any(|term| term == "recovery"));
assert_eq!(results[0].relation_count, 2); assert_eq!(results[1].relation_count, 2);
assert_eq!(results[1].entity.id, recovery.id); assert_eq!(results[1].observation_count, 0);
assert_eq!(results[1].observation_count, 1); assert_eq!(
results[1].max_observation_priority,
ContextObservationPriority::Normal
);
assert!(!results.iter().any(|entry| entry.entity.id == unrelated.id)); assert!(!results.iter().any(|entry| entry.entity.id == unrelated.id));
Ok(()) Ok(())

View File

@@ -23,7 +23,8 @@ use crate::session::output::{
}; };
use crate::session::store::{DaemonActivity, FileActivityOverlap, StateStore}; use crate::session::store::{DaemonActivity, FileActivityOverlap, StateStore};
use crate::session::{ use crate::session::{
DecisionLogEntry, FileActivityEntry, Session, SessionGrouping, SessionMessage, SessionState, ContextObservationPriority, DecisionLogEntry, FileActivityEntry, Session, SessionGrouping,
SessionMessage, SessionState,
}; };
use crate::worktree; use crate::worktree;
@@ -4251,9 +4252,15 @@ impl Dashboard {
summary.warnings.len() summary.warnings.len()
); );
let details = completion_summary_observation_details(summary, session); let details = completion_summary_observation_details(summary, session);
let priority = if observation_type == "failure_summary" {
ContextObservationPriority::High
} else {
ContextObservationPriority::Normal
};
if let Err(error) = self.db.add_session_observation( if let Err(error) = self.db.add_session_observation(
&session.id, &session.id,
observation_type, observation_type,
priority,
&observation_summary, &observation_summary,
&details, &details,
) { ) {
@@ -5358,13 +5365,14 @@ impl Dashboard {
let mut lines = vec!["Relevant memory".to_string()]; let mut lines = vec!["Relevant memory".to_string()];
for entry in entries { for entry in entries {
let mut line = format!( let mut line = format!(
"- #{} [{}] {} | score {} | relations {} | observations {}", "- #{} [{}] {} | score {} | relations {} | observations {} | priority {}",
entry.entity.id, entry.entity.id,
entry.entity.entity_type, entry.entity.entity_type,
truncate_for_dashboard(&entry.entity.name, 60), truncate_for_dashboard(&entry.entity.name, 60),
entry.score, entry.score,
entry.relation_count, entry.relation_count,
entry.observation_count entry.observation_count,
entry.max_observation_priority
); );
if let Some(session_id) = entry.entity.session_id.as_deref() { if let Some(session_id) = entry.entity.session_id.as_deref() {
if session_id != session.id { if session_id != session.id {
@@ -5387,7 +5395,8 @@ impl Dashboard {
if let Ok(observations) = self.db.list_context_observations(Some(entry.entity.id), 1) { if let Ok(observations) = self.db.list_context_observations(Some(entry.entity.id), 1) {
if let Some(observation) = observations.first() { if let Some(observation) = observations.first() {
lines.push(format!( lines.push(format!(
" memory {}", " memory [{}] {}",
observation.priority,
truncate_for_dashboard(&observation.summary, 72) truncate_for_dashboard(&observation.summary, 72)
)); ));
} }
@@ -10534,6 +10543,7 @@ diff --git a/src/lib.rs b/src/lib.rs\n\
Some(&memory.id), Some(&memory.id),
entity.id, entity.id,
"completion_summary", "completion_summary",
ContextObservationPriority::Normal,
"Recovered auth callback incident with billing fallback", "Recovered auth callback incident with billing fallback",
&BTreeMap::new(), &BTreeMap::new(),
)?; )?;
@@ -10542,7 +10552,9 @@ diff --git a/src/lib.rs b/src/lib.rs\n\
assert!(text.contains("Relevant memory")); assert!(text.contains("Relevant memory"));
assert!(text.contains("[file] callback.ts")); assert!(text.contains("[file] callback.ts"));
assert!(text.contains("matches auth, callback, recovery")); assert!(text.contains("matches auth, callback, recovery"));
assert!(text.contains("memory Recovered auth callback incident with billing fallback")); assert!(
text.contains("memory [normal] Recovered auth callback incident with billing fallback")
);
Ok(()) Ok(())
} }