优化: 存储层清债与子轨迹来源迁移

- V44 加 ai_conversations.origin 列(子代理轨迹来源标记)
- V21 断点续迁改造(逐对话进度判定,崩溃重跑补漏)
- 手写事务改 guard.transaction,panic 不残留打开事务
- 软删 CAS 补 deleted_at IS NULL(任务/项目/灵感三处)
- 父任务校验拒回收站挂子,删模块先清依赖边
- 列缺失与未知 status 降级补 warn 留痕,limit=0 返空集
This commit is contained in:
lxy
2026-10-03 15:34:55 +08:00
parent 3f4c914dbf
commit 499aa49e49
10 changed files with 796 additions and 84 deletions
+32 -2
View File
@@ -147,6 +147,10 @@ impl AiMessageRepo {
/// list_by_conversation 一致的 seq 升序)。`before_seq` 可选:指定后只取 seq < before_seq
/// 的消息(滚顶加载更多时的游标,取下一页更早的历史)。
///
/// `limit = 0` 契约:返空集(SQLite LIMIT 0 合法且恒 0 行),不做旧版的 `.max(1)`
/// 静默钳制——请求 0 条却回 1 条会让调用方误判页内有数据。当前唯一调用方传
/// 常量 PAGE_SIZE=50 恒 >0,0 只可能来自未来调用方失误,空集是最不易误导的语义。
///
/// 典型用法:
/// - 首次切入对话:list_recent(conv_id, 50, None) → 最近 50 条
/// - 滚顶加载更多:list_recent(conv_id, 50, Some(最早已加载消息的 seq)) → 再加载 50 条更早的
@@ -160,8 +164,9 @@ impl AiMessageRepo {
let conv_id = conversation_id.to_owned();
tokio::task::spawn_blocking(move || {
let guard = conn.blocking_lock();
// before_seq 有无分两个 SQL(参数化 LIMIT 必须用固定占位,Rust 侧 clamp 防 0)
let limit = limit.max(1) as i64;
// before_seq 有无分两个 SQL(参数化 LIMIT 必须用固定占位);limit=0 直传
// LIMIT 0 返空集(契约见方法文档),不再钳成 1
let limit = limit as i64;
let sql = if before_seq.is_some() {
"SELECT id, conversation_id, seq, role, content, parts, tool_call_id,
tool_calls, model, status, reasoning_content, timestamp, created_at,
@@ -681,4 +686,29 @@ mod tests {
let got = repo.list_by_conversation("c").await.expect("list");
assert_eq!(got.len(), 1, "重复 replace 不应叠加");
}
/// limit=0 契约:返空集(直传 SQLite LIMIT 0),不再旧版 max(1) 钳成 1 条。
/// 锁死语义防回归:请求 0 条却回 1 条会让调用方误判页内有数据。
#[tokio::test]
async fn list_recent_limit_zero_returns_empty() {
let db = Database::open_in_memory().await.expect("open_in_memory");
let repo = AiMessageRepo::new(&db);
repo.insert_batch(vec![
mk_msg("m0", "c", 0, "user", "0"),
mk_msg("m1", "c", 1, "user", "1"),
])
.await
.expect("insert");
let got = repo.list_recent("c", 0, None).await.expect("list limit=0");
assert!(got.is_empty(), "limit=0 无游标应返空集");
let got2 = repo.list_recent("c", 0, Some(5)).await.expect("list limit=0 带游标");
assert!(got2.is_empty(), "limit=0 带游标也应返空集");
// 邻位 sanity:limit=1 行为不受影响,仍返最近 1 条
let got3 = repo.list_recent("c", 1, None).await.expect("list limit=1");
assert_eq!(got3.len(), 1);
assert_eq!(got3[0].seq, 1, "limit=1 应取 seq 最大(最新)一条");
}
}