fix(review): 对抗性审查修复——spawn 竞态窗口、max_run_secs 配置化、删除计数一致性
- 子代理 spawn 顺序调整为先注册 cancel_registry 再写 DB running 记录,消除 reap 误杀刚启动子代理的微秒级竞态窗口 - max_run_secs 接入 AgentConfig 配置项(config.json agents 段),不再硬编码;From 透传 + 全部字面构造点补齐 - delete_messages_by_ids 删除后按活 COUNT 重算 sessions/topics 的 message_count,修复计数列漂移 - cli init 修改 agent 路径保留既有 max_run_secs 而非重置默认
This commit is contained in:
parent
452c5ad0ef
commit
6ac653bf51
@ -2348,6 +2348,7 @@ mod tests {
|
||||
max_tool_iterations: 1,
|
||||
tool_result_max_chars: 100_000,
|
||||
context_tool_result_trim_chars: 100_000,
|
||||
max_run_secs: crate::agent::DEFAULT_MAX_RUN_SECS,
|
||||
max_images_in_context: 1,
|
||||
max_image_age_rounds: 10,
|
||||
}
|
||||
|
||||
@ -1410,6 +1410,7 @@ mod tests {
|
||||
max_tool_iterations: 100,
|
||||
tool_result_max_chars: 100_000,
|
||||
context_tool_result_trim_chars: 2_000,
|
||||
max_run_secs: crate::agent::DEFAULT_MAX_RUN_SECS,
|
||||
max_images_in_context: 10,
|
||||
max_image_age_rounds: 50,
|
||||
};
|
||||
|
||||
@ -8,7 +8,7 @@ pub use agent_loop::{
|
||||
PersistingEmittedMessageHandler, SkillProvider,
|
||||
};
|
||||
pub use context_compressor::ContextCompressor;
|
||||
pub use runtime_config::AgentRuntimeConfig;
|
||||
pub use runtime_config::{AgentRuntimeConfig, DEFAULT_MAX_RUN_SECS};
|
||||
pub use system_prompt::{
|
||||
CompositeSystemPromptProvider, SystemPrompt, SystemPromptContext, SystemPromptProvider,
|
||||
generate_system_env_prompt,
|
||||
|
||||
@ -51,7 +51,7 @@ impl From<LLMProviderConfig> for AgentRuntimeConfig {
|
||||
max_images_in_context: config.max_images_in_context,
|
||||
max_image_age_rounds: config.max_image_age_rounds,
|
||||
max_retries: config.max_retries,
|
||||
max_run_secs: DEFAULT_MAX_RUN_SECS,
|
||||
max_run_secs: config.max_run_secs,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -564,6 +564,7 @@ impl InitWizard {
|
||||
max_tool_iterations: 1000,
|
||||
tool_result_max_chars: 100_000,
|
||||
context_tool_result_trim_chars: 2000,
|
||||
max_run_secs: crate::agent::DEFAULT_MAX_RUN_SECS,
|
||||
};
|
||||
|
||||
let mut agents = existing.agents.clone();
|
||||
@ -629,6 +630,7 @@ impl InitWizard {
|
||||
max_tool_iterations: current_agent.max_tool_iterations,
|
||||
tool_result_max_chars: current_agent.tool_result_max_chars,
|
||||
context_tool_result_trim_chars: current_agent.context_tool_result_trim_chars,
|
||||
max_run_secs: current_agent.max_run_secs,
|
||||
};
|
||||
|
||||
let mut agents = existing.agents.clone();
|
||||
|
||||
@ -571,6 +571,10 @@ pub struct AgentConfig {
|
||||
pub tool_result_max_chars: usize,
|
||||
#[serde(default = "default_context_tool_result_trim_chars")]
|
||||
pub context_tool_result_trim_chars: usize,
|
||||
/// 单次 agent run 的墙钟预算(秒)。超时后优雅退出并提示用户可继续。
|
||||
/// 0 表示不限制。默认见 default_max_run_secs。
|
||||
#[serde(default = "default_max_run_secs")]
|
||||
pub max_run_secs: u64,
|
||||
}
|
||||
|
||||
fn default_max_tool_iterations() -> usize {
|
||||
@ -597,6 +601,10 @@ fn default_max_retries() -> u32 {
|
||||
3
|
||||
}
|
||||
|
||||
fn default_max_run_secs() -> u64 {
|
||||
crate::agent::DEFAULT_MAX_RUN_SECS
|
||||
}
|
||||
|
||||
fn default_mcp_tool_timeout_secs() -> u64 {
|
||||
300
|
||||
}
|
||||
@ -982,6 +990,8 @@ pub struct LLMProviderConfig {
|
||||
pub max_tool_iterations: usize,
|
||||
pub tool_result_max_chars: usize,
|
||||
pub context_tool_result_trim_chars: usize,
|
||||
/// 单次 agent run 墙钟预算(秒,透传自 AgentConfig)。0 表示不限制。
|
||||
pub max_run_secs: u64,
|
||||
/// 图片上下文限制配置
|
||||
pub max_images_in_context: usize,
|
||||
pub max_image_age_rounds: usize,
|
||||
@ -1130,6 +1140,7 @@ impl Config {
|
||||
max_tool_iterations: agent.max_tool_iterations,
|
||||
tool_result_max_chars: agent.tool_result_max_chars,
|
||||
context_tool_result_trim_chars: agent.context_tool_result_trim_chars,
|
||||
max_run_secs: agent.max_run_secs,
|
||||
max_images_in_context: self.image_context.max_images_in_context,
|
||||
max_image_age_rounds: self.image_context.max_image_age_rounds,
|
||||
})
|
||||
|
||||
@ -129,6 +129,7 @@ mod tests {
|
||||
max_tool_iterations: 1,
|
||||
tool_result_max_chars: 100_000,
|
||||
context_tool_result_trim_chars: 20_000,
|
||||
max_run_secs: crate::agent::DEFAULT_MAX_RUN_SECS,
|
||||
max_images_in_context: 1,
|
||||
max_image_age_rounds: 10,
|
||||
}
|
||||
|
||||
@ -68,6 +68,7 @@ mod tests {
|
||||
max_tool_iterations: 1,
|
||||
tool_result_max_chars: 100_000,
|
||||
context_tool_result_trim_chars: 20_000,
|
||||
max_run_secs: crate::agent::DEFAULT_MAX_RUN_SECS,
|
||||
max_images_in_context: 1,
|
||||
max_image_age_rounds: 10,
|
||||
}
|
||||
|
||||
@ -1372,6 +1372,7 @@ mod tests {
|
||||
max_tool_iterations: 1,
|
||||
tool_result_max_chars: 100_000,
|
||||
context_tool_result_trim_chars: 100_000,
|
||||
max_run_secs: crate::agent::DEFAULT_MAX_RUN_SECS,
|
||||
max_images_in_context: 1,
|
||||
max_image_age_rounds: 10,
|
||||
}
|
||||
@ -1696,6 +1697,7 @@ mod tests {
|
||||
max_retries: 3,
|
||||
tool_result_max_chars: 100_000,
|
||||
context_tool_result_trim_chars: 100_000,
|
||||
max_run_secs: crate::agent::DEFAULT_MAX_RUN_SECS,
|
||||
max_images_in_context: 1,
|
||||
max_image_age_rounds: 10,
|
||||
};
|
||||
@ -1754,6 +1756,7 @@ mod tests {
|
||||
max_retries: 3,
|
||||
tool_result_max_chars: 100_000,
|
||||
context_tool_result_trim_chars: 100_000,
|
||||
max_run_secs: crate::agent::DEFAULT_MAX_RUN_SECS,
|
||||
max_images_in_context: 1,
|
||||
max_image_age_rounds: 10,
|
||||
};
|
||||
@ -1833,6 +1836,7 @@ mod tests {
|
||||
max_retries: 3,
|
||||
tool_result_max_chars: 100_000,
|
||||
context_tool_result_trim_chars: 100_000,
|
||||
max_run_secs: crate::agent::DEFAULT_MAX_RUN_SECS,
|
||||
max_images_in_context: 1,
|
||||
max_image_age_rounds: 10,
|
||||
};
|
||||
@ -1910,6 +1914,7 @@ mod tests {
|
||||
max_retries: 3,
|
||||
tool_result_max_chars: 100_000,
|
||||
context_tool_result_trim_chars: 100_000,
|
||||
max_run_secs: crate::agent::DEFAULT_MAX_RUN_SECS,
|
||||
max_images_in_context: 1,
|
||||
max_image_age_rounds: 10,
|
||||
};
|
||||
@ -2051,6 +2056,7 @@ mod tests {
|
||||
max_retries: 3,
|
||||
tool_result_max_chars: 100_000,
|
||||
context_tool_result_trim_chars: 100_000,
|
||||
max_run_secs: crate::agent::DEFAULT_MAX_RUN_SECS,
|
||||
max_images_in_context: 1,
|
||||
max_image_age_rounds: 10,
|
||||
};
|
||||
@ -2123,6 +2129,7 @@ mod tests {
|
||||
max_retries: 3,
|
||||
tool_result_max_chars: 100_000,
|
||||
context_tool_result_trim_chars: 100_000,
|
||||
max_run_secs: crate::agent::DEFAULT_MAX_RUN_SECS,
|
||||
max_images_in_context: 1,
|
||||
max_image_age_rounds: 10,
|
||||
};
|
||||
@ -2205,6 +2212,7 @@ mod tests {
|
||||
max_retries: 3,
|
||||
tool_result_max_chars: 100_000,
|
||||
context_tool_result_trim_chars: 100_000,
|
||||
max_run_secs: crate::agent::DEFAULT_MAX_RUN_SECS,
|
||||
max_images_in_context: 1,
|
||||
max_image_age_rounds: 10,
|
||||
};
|
||||
@ -2296,6 +2304,7 @@ mod tests {
|
||||
max_retries: 3,
|
||||
tool_result_max_chars: 100_000,
|
||||
context_tool_result_trim_chars: 100_000,
|
||||
max_run_secs: crate::agent::DEFAULT_MAX_RUN_SECS,
|
||||
max_images_in_context: 1,
|
||||
max_image_age_rounds: 10,
|
||||
};
|
||||
@ -2386,6 +2395,7 @@ mod tests {
|
||||
max_retries: 3,
|
||||
tool_result_max_chars: 100_000,
|
||||
context_tool_result_trim_chars: 100_000,
|
||||
max_run_secs: crate::agent::DEFAULT_MAX_RUN_SECS,
|
||||
max_images_in_context: 1,
|
||||
max_image_age_rounds: 10,
|
||||
};
|
||||
@ -2458,6 +2468,7 @@ mod tests {
|
||||
max_retries: 3,
|
||||
tool_result_max_chars: 100_000,
|
||||
context_tool_result_trim_chars: 100_000,
|
||||
max_run_secs: crate::agent::DEFAULT_MAX_RUN_SECS,
|
||||
max_images_in_context: 1,
|
||||
max_image_age_rounds: 10,
|
||||
};
|
||||
@ -2540,6 +2551,7 @@ mod tests {
|
||||
max_retries: 3,
|
||||
tool_result_max_chars: 100_000,
|
||||
context_tool_result_trim_chars: 100_000,
|
||||
max_run_secs: crate::agent::DEFAULT_MAX_RUN_SECS,
|
||||
max_images_in_context: 1,
|
||||
max_image_age_rounds: 10,
|
||||
};
|
||||
@ -2608,6 +2620,7 @@ mod tests {
|
||||
max_retries: 3,
|
||||
tool_result_max_chars: 100_000,
|
||||
context_tool_result_trim_chars: 100_000,
|
||||
max_run_secs: crate::agent::DEFAULT_MAX_RUN_SECS,
|
||||
max_images_in_context: 1,
|
||||
max_image_age_rounds: 10,
|
||||
};
|
||||
|
||||
@ -642,10 +642,30 @@ impl SessionStore {
|
||||
}
|
||||
let conn = self.pool.get()?;
|
||||
let mut total = 0;
|
||||
let mut affected_topics: Vec<String> = Vec::new();
|
||||
// SQLite 绑定变量上限(SQLITE_MAX_VARIABLE_NUMBER 默认 999),
|
||||
// 分批构造 IN 子句,避免超限。
|
||||
for chunk in ids.chunks(500) {
|
||||
let placeholders = vec!["?"; chunk.len()].join(",");
|
||||
|
||||
// 先收集将被删除行的 topic_id,用于删除后重算 topics.message_count
|
||||
let topic_sql = format!(
|
||||
"SELECT DISTINCT topic_id FROM messages
|
||||
WHERE session_id = ? AND topic_id IS NOT NULL AND id IN ({placeholders})"
|
||||
);
|
||||
let mut topic_params: Vec<&dyn rusqlite::ToSql> = Vec::with_capacity(chunk.len() + 1);
|
||||
topic_params.push(&session_id);
|
||||
for id in chunk {
|
||||
topic_params.push(id);
|
||||
}
|
||||
let mut stmt = conn.prepare(&topic_sql)?;
|
||||
let rows = stmt.query_map(topic_params.as_slice(), |row| {
|
||||
row.get::<_, String>(0)
|
||||
})?;
|
||||
for tid in rows {
|
||||
affected_topics.push(tid?);
|
||||
}
|
||||
|
||||
let sql = format!(
|
||||
"DELETE FROM messages WHERE session_id = ? AND id IN ({placeholders})"
|
||||
);
|
||||
@ -656,6 +676,28 @@ impl SessionStore {
|
||||
}
|
||||
total += conn.execute(&sql, params_vec.as_slice())?;
|
||||
}
|
||||
|
||||
// 计数列与消息行的一致性:sessions/topics 的 message_count 是增量维护列
|
||||
// (append +1、clear 归零),删除后必须同步。用活 COUNT 重算而非减法,
|
||||
// 自愈历史漂移且不会减为负数。
|
||||
if total > 0 {
|
||||
conn.execute(
|
||||
"UPDATE sessions SET message_count =
|
||||
(SELECT COUNT(*) FROM messages WHERE session_id = ?1)
|
||||
WHERE id = ?1",
|
||||
params![session_id],
|
||||
)?;
|
||||
affected_topics.sort_unstable();
|
||||
affected_topics.dedup();
|
||||
for tid in &affected_topics {
|
||||
conn.execute(
|
||||
"UPDATE topics SET message_count =
|
||||
(SELECT COUNT(*) FROM messages WHERE topic_id = ?1)
|
||||
WHERE id = ?1",
|
||||
params![tid],
|
||||
)?;
|
||||
}
|
||||
}
|
||||
Ok(total)
|
||||
}
|
||||
|
||||
|
||||
@ -981,6 +981,8 @@ fn test_delete_messages_by_ids_removes_only_target_rows() {
|
||||
.unwrap(),
|
||||
0
|
||||
);
|
||||
// 消息计数同步修正
|
||||
// 消息计数同步修正(活查询 + 计数列均一致)
|
||||
assert_eq!(store.get_topic_message_count(&topic.id).unwrap(), 3);
|
||||
let session_after = store.get_session(&session.id).unwrap().unwrap();
|
||||
assert_eq!(session_after.message_count, 3);
|
||||
}
|
||||
|
||||
@ -134,6 +134,7 @@ mod tests {
|
||||
max_tool_iterations: 1,
|
||||
tool_result_max_chars: 1000,
|
||||
context_tool_result_trim_chars: 1000,
|
||||
max_run_secs: crate::agent::DEFAULT_MAX_RUN_SECS,
|
||||
max_images_in_context: 1,
|
||||
max_image_age_rounds: 10,
|
||||
},
|
||||
@ -168,6 +169,7 @@ mod tests {
|
||||
max_tool_iterations: 1,
|
||||
tool_result_max_chars: 1000,
|
||||
context_tool_result_trim_chars: 1000,
|
||||
max_run_secs: crate::agent::DEFAULT_MAX_RUN_SECS,
|
||||
max_images_in_context: 1,
|
||||
max_image_age_rounds: 10,
|
||||
},
|
||||
|
||||
@ -1039,7 +1039,18 @@ impl SubAgentRuntime for DefaultSubAgentRuntime {
|
||||
let topic_id = session.parent_topic_id.clone().unwrap_or_default();
|
||||
let task_id = session.id.clone();
|
||||
|
||||
// 8a. INSERT pending_subagents 记录
|
||||
// 8a. 注册 cancel_registry(必须先于 DB 写入 running 记录)。
|
||||
// reap_orphan_subagents 以"registry 中存在条目"判定执行任务存活,
|
||||
// 因此不变量为:DB 出现 running 记录时 registry 必然已有条目。
|
||||
// 若顺序颠倒,spawn 间隙(DB 已写、registry 未写)的并发 reap
|
||||
// 会把刚启动的子代理误标为 interrupted。
|
||||
let cancel_registry = self.cancel_registry.clone();
|
||||
let cancel_token = tokio_util::sync::CancellationToken::new();
|
||||
cancel_registry
|
||||
.lock()
|
||||
.insert(task_id.clone(), cancel_token.clone());
|
||||
|
||||
// 8b. INSERT pending_subagents 记录
|
||||
let pending_record = PendingSubagentRecord {
|
||||
task_id: task_id.clone(),
|
||||
parent_session_id: session.parent_session_id.clone(),
|
||||
@ -1058,7 +1069,7 @@ impl SubAgentRuntime for DefaultSubAgentRuntime {
|
||||
);
|
||||
}
|
||||
|
||||
// 8b. tokio::spawn 后台执行子代理
|
||||
// 8c. tokio::spawn 后台执行子代理
|
||||
let store = self.store.clone();
|
||||
let task_repository = self.task_repository.clone();
|
||||
let bus = self.bus.clone();
|
||||
@ -1070,16 +1081,10 @@ impl SubAgentRuntime for DefaultSubAgentRuntime {
|
||||
let max_secs = self.effective_max_execution_secs(&def);
|
||||
let task_id_for_spawn = task_id.clone();
|
||||
let semaphore = self.semaphore.clone();
|
||||
let cancel_registry = self.cancel_registry.clone();
|
||||
|
||||
// 创建 CancellationToken 并注册到 registry(供 /stop 取消传播)
|
||||
let cancel_token = tokio_util::sync::CancellationToken::new();
|
||||
cancel_registry
|
||||
.lock()
|
||||
.insert(task_id_for_spawn.clone(), cancel_token.clone());
|
||||
|
||||
// RAII guard:spawn 任务退出时(正常/early return/panic)确定性清理 registry
|
||||
// 不变量 3:清理与作用域绑定,避免末行清理被 panic 绕过
|
||||
// (cancel_token 已在 8a 注册,此处仅绑定生命周期清理)
|
||||
let registry_guard =
|
||||
CancelRegistryGuard::new(task_id_for_spawn.clone(), cancel_registry.clone());
|
||||
|
||||
|
||||
@ -45,6 +45,7 @@ fn load_config() -> Option<LLMProviderConfig> {
|
||||
max_tool_iterations: 20,
|
||||
tool_result_max_chars: 20_000,
|
||||
context_tool_result_trim_chars: 20_000,
|
||||
max_run_secs: picobot::agent::DEFAULT_MAX_RUN_SECS,
|
||||
max_images_in_context: 1,
|
||||
max_image_age_rounds: 10,
|
||||
})
|
||||
|
||||
@ -47,6 +47,7 @@ fn load_openai_config() -> Option<LLMProviderConfig> {
|
||||
max_tool_iterations: 20,
|
||||
tool_result_max_chars: 20_000,
|
||||
context_tool_result_trim_chars: 20_000,
|
||||
max_run_secs: picobot::agent::DEFAULT_MAX_RUN_SECS,
|
||||
max_images_in_context: 1,
|
||||
max_image_age_rounds: 10,
|
||||
})
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user