feat: 更新调度配置以每4小时执行内存维护,并添加根据更新时间过滤内存范围的功能

This commit is contained in:
ooodc 2026-04-27 13:05:55 +08:00
parent 52c94f274a
commit 4fb102644e
4 changed files with 142 additions and 9 deletions

View File

@ -311,7 +311,7 @@ impl SchedulerConfig {
enabled: true,
kind: SchedulerJobKind::InternalEvent,
schedule: Some(SchedulerSchedule::Cron {
expression: "0 3 * * *".to_string(),
expression: "0 */4 * * *".to_string(),
}),
startup_delay_secs: 0,
interval_secs: 0,
@ -319,7 +319,7 @@ impl SchedulerConfig {
payload: serde_json::json!({
"event": "memory_maintenance",
"time_zone": time.timezone,
"local_time": "03:00"
"local_time": "every_4_hours"
}),
}]
}
@ -1146,7 +1146,7 @@ mod tests {
assert_eq!(
effective_jobs[0].resolved_schedule().unwrap(),
SchedulerSchedule::Cron {
expression: "0 3 * * *".to_string(),
expression: "0 */4 * * *".to_string(),
}
);
}

View File

@ -1168,11 +1168,17 @@ impl SessionManager {
pub(crate) async fn run_memory_maintenance_for_all_scopes(
&self,
updated_since: Option<i64>,
) -> Result<Vec<MemoryMaintenanceScopeResult>, AgentError> {
let scope_keys = self
.store
.list_memory_scope_keys("user")
.map_err(|err| AgentError::Other(format!("list memory scope keys error: {}", err)))?;
let scope_keys = if let Some(cutoff) = updated_since {
self.store
.list_memory_scope_keys_updated_since("user", cutoff)
.map_err(|err| AgentError::Other(format!("list memory scope keys updated since error: {}", err)))?
} else {
self.store
.list_memory_scope_keys("user")
.map_err(|err| AgentError::Other(format!("list memory scope keys error: {}", err)))?
};
let mut results = Vec::new();
for scope_key in scope_keys {
@ -2250,6 +2256,60 @@ mod tests {
assert!(output.managed_markdown.contains("### 用户事实"));
}
#[tokio::test]
async fn test_run_memory_maintenance_for_all_scopes_returns_empty_when_no_recent_updates() {
let provider_config = LLMProviderConfig {
provider_type: "openai".to_string(),
name: "maintenance-provider".to_string(),
base_url: "http://localhost".to_string(),
api_key: "test-key".to_string(),
extra_headers: HashMap::new(),
model_id: "maintenance-model".to_string(),
temperature: Some(0.0),
max_tokens: Some(256),
model_extra: HashMap::new(),
max_tool_iterations: 1,
llm_timeout_secs: 30,
tool_result_max_chars: 20_000,
context_tool_result_trim_chars: 20_000,
};
let session_manager = SessionManager::new(
4,
100,
false,
"Asia/Shanghai".to_string(),
provider_config.clone(),
HashMap::from([("default".to_string(), provider_config)]),
Arc::new(SkillRuntime::default()),
)
.unwrap();
let memory = session_manager
.store()
.put_memory(&crate::storage::MemoryUpsert {
scope_kind: "user".to_string(),
scope_key: "feishu:user-1".to_string(),
namespace: "profile".to_string(),
memory_key: "work".to_string(),
content: "用户在做AI产品".to_string(),
source_type: "message".to_string(),
source_session_id: None,
source_message_id: None,
source_message_seq: None,
source_channel_name: None,
source_chat_id: None,
})
.unwrap();
let results = session_manager
.run_memory_maintenance_for_all_scopes(Some(memory.updated_at + 1))
.await
.unwrap();
assert!(results.is_empty());
}
#[test]
fn test_apply_memory_maintenance_output_merges_and_deletes_low_value_records() {
let store = SessionStore::in_memory().unwrap();

View File

@ -501,7 +501,9 @@ async fn execute_internal_event(session_manager: &SessionManager, job: &RuntimeJ
Ok(())
}
"memory_maintenance" => {
let results = session_manager.run_memory_maintenance_for_all_scopes().await?;
let results = session_manager
.run_memory_maintenance_for_all_scopes(job.last_fired_at)
.await?;
for result in &results {
tracing::info!(
job_id = %job.id,
@ -937,9 +939,10 @@ mod tests {
saved.schedule,
serde_json::json!({
"type": "cron",
"expression": "0 3 * * *"
"expression": "0 */4 * * *"
})
);
assert_eq!(saved.payload.get("local_time").and_then(|value| value.as_str()), Some("every_4_hours"));
assert!(saved.next_fire_at.is_some());
}

View File

@ -925,6 +925,29 @@ impl SessionStore {
Ok(scope_keys)
}
pub fn list_memory_scope_keys_updated_since(
&self,
scope_kind: &str,
since_timestamp: i64,
) -> Result<Vec<String>, StorageError> {
let conn = self.conn.lock().expect("session db mutex poisoned");
let mut stmt = conn.prepare(
"
SELECT DISTINCT scope_key
FROM memories
WHERE scope_kind = ?1 AND updated_at > ?2
ORDER BY scope_key ASC
",
)?;
let rows = stmt.query_map(params![scope_kind, since_timestamp], |row| row.get::<_, String>(0))?;
let mut scope_keys = Vec::new();
for row in rows {
scope_keys.push(row?);
}
Ok(scope_keys)
}
pub fn list_memories_for_scope(
&self,
scope_kind: &str,
@ -2429,4 +2452,51 @@ mod tests {
assert_eq!(fetched.run_count, 1);
assert_eq!(fetched.completed_at, Some(1_700_000_000_100));
}
#[test]
fn test_list_memory_scope_keys_updated_since_filters_recent_scopes() {
let store = SessionStore::in_memory().unwrap();
let first = store
.put_memory(&MemoryUpsert {
scope_kind: "user".to_string(),
scope_key: "feishu:user-1".to_string(),
namespace: "profile".to_string(),
memory_key: "work".to_string(),
content: "用户在做AI产品".to_string(),
source_type: "message".to_string(),
source_session_id: None,
source_message_id: None,
source_message_seq: None,
source_channel_name: None,
source_chat_id: None,
})
.unwrap();
let cutoff = first.updated_at;
std::thread::sleep(std::time::Duration::from_millis(2));
store
.put_memory(&MemoryUpsert {
scope_kind: "user".to_string(),
scope_key: "feishu:user-2".to_string(),
namespace: "preferences".to_string(),
memory_key: "style".to_string(),
content: "偏好简洁表达".to_string(),
source_type: "message".to_string(),
source_session_id: None,
source_message_id: None,
source_message_seq: None,
source_channel_name: None,
source_chat_id: None,
})
.unwrap();
let recent_scope_keys = store
.list_memory_scope_keys_updated_since("user", cutoff)
.unwrap();
assert_eq!(recent_scope_keys, vec!["feishu:user-2".to_string()]);
}
}