Skip to main content

agentmux_srv/backend/storage/
agent_activity_summaries.rs

1// Copyright 2026, AgentMux Corp.
2// SPDX-License-Identifier: Apache-2.0
3
4//! On-demand, once-per-definition activity summary โ€” see
5//! `db_agent_activity_summaries` in migrations.rs (`OBJECT_SCHEMA_VERSION`
6//! v27) and
7//! docs/reports/REPORT_AGENT_PICKER_FIELD_ORDER_SORT_AND_DATA_GAPS_AUDIT_2026_08_24.md ยง5a.
8//!
9//! Fallback preview text for the AgentPicker's "My Agents" rows whose
10//! instance has no structured `output.state.json` conversation snapshot โ€”
11//! generated once, lazily, via `crate::ambient::tasks::generate_definition_activity_summary`,
12//! and cached here forever (never regenerated once non-empty). An absent
13//! row IS the "not generated yet" state; there is no separate pending flag.
14
15use rusqlite::params;
16
17use super::error::StoreError;
18use super::store::Store;
19
20#[derive(Debug, Clone, PartialEq, Eq)]
21pub struct AgentActivitySummary {
22    pub definition_id: String,
23    pub summary: String,
24    pub updated_at: i64,
25}
26
27impl Store {
28    pub fn agent_activity_summary_get(
29        &self,
30        definition_id: &str,
31    ) -> Result<Option<AgentActivitySummary>, StoreError> {
32        let conn = self.conn.lock().unwrap();
33        let mut stmt = conn.prepare(
34            "SELECT definition_id, summary, updated_at FROM db_agent_activity_summaries WHERE definition_id = ?1",
35        )?;
36        let mut rows = stmt.query_map(params![definition_id], map_row)?;
37        match rows.next() {
38            Some(r) => Ok(Some(r?)),
39            None => Ok(None),
40        }
41    }
42
43    /// Upsert โ€” a definition's summary is generated at most once in the
44    /// common case, but an upsert (rather than plain INSERT) keeps this
45    /// safe against the rare double-generation race the Ambient Model Call
46    /// gateway doesn't fully close (two independent process starts, or a
47    /// restart mid-generation).
48    pub fn agent_activity_summary_set(
49        &self,
50        definition_id: &str,
51        summary: &str,
52        updated_at: i64,
53    ) -> Result<(), StoreError> {
54        let conn = self.conn.lock().unwrap();
55        conn.execute(
56            "INSERT INTO db_agent_activity_summaries (definition_id, summary, updated_at)
57             VALUES (?1, ?2, ?3)
58             ON CONFLICT(definition_id) DO UPDATE SET summary = ?2, updated_at = ?3",
59            params![definition_id, summary, updated_at],
60        )?;
61        Ok(())
62    }
63}
64
65fn map_row(row: &rusqlite::Row<'_>) -> rusqlite::Result<AgentActivitySummary> {
66    Ok(AgentActivitySummary {
67        definition_id: row.get(0)?,
68        summary: row.get(1)?,
69        updated_at: row.get(2)?,
70    })
71}
72
73#[cfg(test)]
74mod tests {
75    use super::*;
76
77    fn object_store() -> Store {
78        Store::open(std::path::Path::new(":memory:")).unwrap()
79    }
80
81    #[test]
82    fn get_returns_none_when_never_generated() {
83        let store = object_store();
84        assert!(store.agent_activity_summary_get("def-1").unwrap().is_none());
85    }
86
87    #[test]
88    fn set_then_get_roundtrips() {
89        let store = object_store();
90        store.agent_activity_summary_set("def-1", "Debugging a flaky test", 1000).unwrap();
91        let got = store.agent_activity_summary_get("def-1").unwrap().unwrap();
92        assert_eq!(got.definition_id, "def-1");
93        assert_eq!(got.summary, "Debugging a flaky test");
94        assert_eq!(got.updated_at, 1000);
95    }
96
97    #[test]
98    fn set_twice_upserts_instead_of_erroring() {
99        let store = object_store();
100        store.agent_activity_summary_set("def-1", "first summary", 1000).unwrap();
101        store.agent_activity_summary_set("def-1", "second summary", 2000).unwrap();
102        let got = store.agent_activity_summary_get("def-1").unwrap().unwrap();
103        assert_eq!(got.summary, "second summary");
104        assert_eq!(got.updated_at, 2000);
105    }
106
107    #[test]
108    fn distinct_definitions_do_not_collide() {
109        let store = object_store();
110        store.agent_activity_summary_set("def-1", "summary one", 1000).unwrap();
111        store.agent_activity_summary_set("def-2", "summary two", 2000).unwrap();
112        assert_eq!(store.agent_activity_summary_get("def-1").unwrap().unwrap().summary, "summary one");
113        assert_eq!(store.agent_activity_summary_get("def-2").unwrap().unwrap().summary, "summary two");
114    }
115}