Skip to content

Commit 90ba5e1

Browse files
committed
fix(tasks): AI 任务 id 序列以 DB 全表最大 id 为基准防重启复用顶替历史行
启动恢复仅按「succeeded 且未采纳」恢复集越过 id 序列,已采纳/failed/ proofread/goal_plan 等 id 更大的历史行被漏掉——重启后新任务复用旧 task_id,insert_ai_task 的 INSERT OR REPLACE 静默顶替历史行(含已采纳 的结果/成本/轨迹)。基准改查 ai_tasks 全表 MAX(max_ai_task_id,空表 NULL→0),推进下限为纯函数 task_seq_lower_bound(saturating 防溢出, 序列只前进不回退);查询失败降级旧恢复集口径不放大风险。新增单测: 全状态行 max(回归形状 80/95/120→120)、空表 0、序列下限边界。
1 parent ce46730 commit 90ba5e1

4 files changed

Lines changed: 77 additions & 6 deletions

File tree

‎app/src-tauri/src/app_setup.rs‎

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -196,8 +196,11 @@ pub fn setup_app_state(app: &mut tauri::App) -> Result<(), String> {
196196
// 执行,前端轮询/事件双通道——禁止同步阻塞 30s+ 长会话精修)
197197
let ai_tasks = crate::commands_ai_refine::task_registry();
198198
let ai_task_seq = crate::commands_ai_refine::task_seq();
199-
// v0.8.0 F2(2026-08-21):任务中心——启动恢复未采纳的成功结果
200-
// (重启不丢;注册表 + id 序列以恢复结果为基准,防 id 冲突覆盖)
199+
// v0.8.0 F2(2026-08-21):任务中心——启动恢复未采纳的成功结果(重启
200+
// 不丢;注册表以恢复集为准)。id 序列以 **DB 全表最大 id** 为基准防冲突
201+
// (2026-09-09 批 1 修复:旧口径只按恢复集 max+1,会漏掉已采纳/failed/
202+
// proofread/goal_plan 等 id 更大的历史行——重启后新任务复用旧 task_id,
203+
// insert_ai_task 的 INSERT OR REPLACE 静默顶替历史行含已采纳,见下)
201204
{
202205
// 保留策略先行(清理超限旧终态——防表膨胀)
203206
let _ = db.trim_ai_tasks();
@@ -207,8 +210,13 @@ pub fn setup_app_state(app: &mut tauri::App) -> Result<(), String> {
207210
eprintln!("[ai-tasks] 恢复失败(注册表空启动): {}", e);
208211
Vec::new()
209212
});
213+
// 序列基准 = DB 全表 max(覆盖所有状态行);查询失败降级为恢复集
214+
// 口径(旧行为——DB 不可读时新任务也难落库,风险不放大)
215+
let db_max_task_id = db.max_ai_task_id().unwrap_or_else(|e| {
216+
eprintln!("[ai-tasks] 查询任务表最大 id 失败(按恢复集口径推进序列): {}", e);
217+
restored.iter().map(|r| r.task_id).max().unwrap_or(0)
218+
});
210219
if let Ok(mut tasks) = ai_tasks.lock() {
211-
let mut max_id = 0u64;
212220
for rec in &restored {
213221
let result = rec
214222
.result_json
@@ -223,13 +231,13 @@ pub fn setup_app_state(app: &mut tauri::App) -> Result<(), String> {
223231
target_id: rec.ref_id,
224232
},
225233
);
226-
max_id = max_id.max(rec.task_id);
227234
}
228-
// id 序列越过恢复的最大 id——新任务不复用旧 id(防覆盖已恢复结果)
235+
// id 序列越过 DB 最大 id——新任务不复用任何历史行 id
236+
// (防 INSERT OR REPLACE 顶替已采纳/失败等历史行)
229237
let _ = ai_task_seq.fetch_update(
230238
std::sync::atomic::Ordering::Relaxed,
231239
std::sync::atomic::Ordering::Relaxed,
232-
|cur| Some(cur.max(max_id + 1)),
240+
|cur| Some(crate::commands_ai_refine::task_seq_lower_bound(cur, db_max_task_id)),
233241
);
234242
eprintln!("[ai-tasks] 启动恢复 {} 条未采纳任务", restored.len());
235243
}

‎app/src-tauri/src/commands_ai_refine.rs‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -604,6 +604,18 @@ pub fn task_seq() -> Arc<AtomicU64> {
604604
Arc::new(AtomicU64::new(1))
605605
}
606606

607+
/// id 序列下限(纯函数):取「当前值」与「DB 最大 task_id + 1」之大。
608+
///
609+
/// @ai-context Why(2026-09-09 批 1 修复):启动恢复后序列必须越过 **DB
610+
/// 全表** 最大 id——恢复集只含未采纳成功任务,已采纳/failed/
611+
/// proofread/goal_plan 行 id 更大时,若只按恢复集推进,新任务
612+
/// 将复用历史 task_id 并被 insert_ai_task 的 INSERT OR REPLACE
613+
/// 静默顶替(历史行含已采纳——数据丢失)。saturating 防极端 id
614+
/// 溢出;序列只前进不回退(当前值更大时保持)。
615+
pub fn task_seq_lower_bound(current: u64, db_max_task_id: u64) -> u64 {
616+
current.max(db_max_task_id.saturating_add(1))
617+
}
618+
607619
/// 任务注册表(AppState 装配)。
608620
pub fn task_registry() -> Arc<Mutex<HashMap<u64, AiTaskEntry>>> {
609621
Arc::new(Mutex::new(HashMap::new()))

‎app/src-tauri/src/db_ai_tasks.rs‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,24 @@ impl Db {
204204
.map_err(Into::into)
205205
}
206206

207+
/// 表内当前最大 task_id(应用启动时 id 序列基准;空表 → 0)。
208+
///
209+
/// @ai-context Why(2026-09-09 批 1 修复):恢复列表只含「succeeded 且
210+
/// 未采纳」行,而已采纳/failed/proofread/goal_plan 等行的
211+
/// id 往往更大——启动序列若只越过恢复集 max,重启后新任务
212+
/// 会复用历史 task_id,insert_ai_task 的 INSERT OR REPLACE
213+
/// 静默顶替历史行(含已采纳的结果/成本/轨迹——数据丢失)。
214+
/// 基准改查全表 max:覆盖全部状态的行,序列只前进不回退。
215+
pub fn max_ai_task_id(&self) -> Result<u64> {
216+
let conn = self.conn.lock().unwrap_or_else(|e| e.into_inner());
217+
// 空表时 MAX() 返回单行 NULL——须 Option<i64> 读取(rusqlite 严格类型,
218+
// 教训同 get_ai_task_trajectory 的 NULL 列修复),再拍平为 0
219+
let max: Option<Option<i64>> = conn
220+
.query_row("SELECT MAX(task_id) FROM ai_tasks", [], |r| r.get(0))
221+
.optional()?;
222+
Ok(max.flatten().unwrap_or(0).max(0) as u64)
223+
}
224+
207225
/// 恢复未采纳的成功结果(应用启动时;供任务面板 + 结果重取)。
208226
pub fn list_restorable_succeeded(&self, limit: usize) -> Result<Vec<AiTaskRecord>> {
209227
let conn = self.conn.lock().unwrap_or_else(|e| e.into_inner());

‎app/src-tauri/src/db_ai_tasks_tests.rs‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@
22
//!
33
//! @ai-context: 覆盖:插入/终态更新/采纳标记/成本回填/恢复(未采纳成功
44
//! 结果)/历史列表/保留策略(每类型 50 条上限清理最旧)。
5+
//! @ai-context 2026-09-09 批 1:任务表最大 id 查询 + 启动序列下限纯函数
6+
//! (防重启后新任务复用历史 task_id 顶替历史行——含已采纳)。
57
8+
use crate::commands_ai_refine::task_seq_lower_bound;
69
use crate::db::Db;
710
use crate::db_ai_tasks::{AiTaskRecord, TASKS_KEEP_PER_TYPE};
811

@@ -150,3 +153,33 @@ fn adopted_query_true_after_mark_false_otherwise() {
150153
// 不存在的任务 → false(防御方向保守——旧任务无记录放行)
151154
assert!(!db.is_ai_task_adopted(999));
152155
}
156+
157+
#[test]
158+
fn max_task_id_spans_all_states_not_only_restorable() {
159+
// Arrange(2026-09-09 批 1 回归形状):id 更大的已采纳/failed/running
160+
// 行均不在启动恢复集——旧实现按恢复集越序列会漏掉它们,重启后新任务
161+
// 复用这些 task_id 并 REPLACE 顶替(含已采纳——数据丢失)
162+
let db = open_mem();
163+
let mut adopted = rec(80, "refine", 1, "succeeded");
164+
adopted.adopted = true;
165+
db.insert_ai_task(&adopted).unwrap();
166+
db.insert_ai_task(&rec(120, "refine", 2, "failed")).unwrap();
167+
db.insert_ai_task(&rec(95, "enrich", 3, "running")).unwrap();
168+
// Act + Assert:全表最大 id = 120(不被恢复集 80 封顶)
169+
assert_eq!(db.max_ai_task_id().unwrap(), 120);
170+
// 空表 → 0(序列下限不后退,起点维持 1)
171+
assert_eq!(open_mem().max_ai_task_id().unwrap(), 0);
172+
}
173+
174+
#[test]
175+
fn seq_lower_bound_never_reuses_existing_row_ids() {
176+
// 空表(db_max=0)→ 至少 1(与 task_seq 初始值一致)
177+
assert_eq!(task_seq_lower_bound(1, 0), 1);
178+
// DB 最大 id 41(如已采纳历史行)→ 下限 42:新任务不复用 1..=41
179+
assert_eq!(task_seq_lower_bound(1, 41), 42);
180+
assert_eq!(task_seq_lower_bound(10, 41), 42);
181+
// 当前值已更大 → 只前进不回退
182+
assert_eq!(task_seq_lower_bound(300, 41), 300);
183+
// 极端:u64::MAX 不 panic(saturating_add)且不前进
184+
assert_eq!(task_seq_lower_bound(u64::MAX, u64::MAX), u64::MAX);
185+
}

0 commit comments

Comments
 (0)