fix: TaskStore snapshot を compact 後 history の末尾に置いて retained 中の TaskCreate 重複を防ぐ

This commit is contained in:
Keisuke Hirata 2026-05-03 21:26:49 +09:00
parent 6f2aca84bf
commit ceafff92b6
No known key found for this signature in database
2 changed files with 43 additions and 5 deletions

View File

@ -1395,7 +1395,12 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
.filter(|i| i.is_user_message()) .filter(|i| i.is_user_message())
.count(); .count();
// Build new history: [summary, ...auto-read, task snapshot, TaskList result, references, ...retained]. // Build new history: [summary, ...auto-read, references, ...retained, task snapshot, TaskList synthetic call/result].
// The TaskStore snapshot trails the retained items so that, on resume,
// `replay_history` walks any pre-compact Task* calls preserved verbatim
// in retained_items first and the trailing snapshot's `replace_with`
// is the final word — pre-compact `TaskCreate` calls cannot leak as
// duplicate entries.
let mut new_history = Vec::with_capacity( let mut new_history = Vec::with_capacity(
1 + auto_read_messages.len() 1 + auto_read_messages.len()
+ 3 + 3
@ -1406,6 +1411,10 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
"[Compacted context summary]\n\n{summary_text}" "[Compacted context summary]\n\n{summary_text}"
))); )));
new_history.extend(auto_read_messages); new_history.extend(auto_read_messages);
if let Some(msg) = reference_message {
new_history.push(msg);
}
new_history.extend(retained_items);
new_history.push(Item::system_message(format!( new_history.push(Item::system_message(format!(
"[Session TaskStore snapshot]\n\n{task_snapshot_text}\n\n\ "[Session TaskStore snapshot]\n\n{task_snapshot_text}\n\n\
This is the complete session task list preserved across compaction. \ This is the complete session task list preserved across compaction. \
@ -1417,10 +1426,6 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
tools::task::snapshot_overview(&self.task_store.list()), tools::task::snapshot_overview(&self.task_store.list()),
task_snapshot_text.clone(), task_snapshot_text.clone(),
)); ));
if let Some(msg) = reference_message {
new_history.push(msg);
}
new_history.extend(retained_items);
// Persist as a new compacted session. // Persist as a new compacted session.
let old_session_id = self.session_id; let old_session_id = self.session_id;

View File

@ -609,4 +609,37 @@ mod tests {
assert_eq!(tasks[1].taskid, 2); assert_eq!(tasks[1].taskid, 2);
assert_eq!(tasks[1].subject, "new"); assert_eq!(tasks[1].subject, "new");
} }
#[test]
fn trailing_snapshot_supersedes_pre_compact_taskcreates_in_retained() {
// Mirrors the post-compact layout: pre-compact `TaskCreate` calls are
// preserved verbatim in retained_items, and the snapshot trails them.
// The trailing snapshot must reset the store to the captured state so
// pre-compact `TaskCreate`s do not surface as duplicates.
let snapshot = "[Session TaskStore snapshot]\n\nTaskStore: 2 task(s) (pending: 0, inprogress: 1, completed: 1, deleted: 0)\n\n- taskid: 1\n status: completed\n subject: A\n description: A-desc\n\n- taskid: 2\n status: inprogress\n subject: B\n description: B-desc\n";
let history = vec![
Item::tool_call("c1", "TaskCreate", r#"{"subject":"A","description":"A-desc"}"#),
Item::tool_call("u1", "TaskUpdate", r#"{"taskid":1,"status":"completed"}"#),
Item::tool_call("c2", "TaskCreate", r#"{"subject":"B","description":"B-desc"}"#),
Item::tool_call("u2", "TaskUpdate", r#"{"taskid":2,"status":"inprogress"}"#),
Item::system_message(snapshot),
Item::tool_call("compact-tasklist", "TaskList", "{}"),
Item::tool_call(
"c3",
"TaskCreate",
r#"{"subject":"C","description":"after compact"}"#,
),
];
let store = TaskStore::from_history(&history);
let tasks = store.list();
assert_eq!(tasks.len(), 3);
assert_eq!(tasks[0].taskid, 1);
assert_eq!(tasks[0].subject, "A");
assert_eq!(tasks[0].status, TaskStatus::Completed);
assert_eq!(tasks[1].taskid, 2);
assert_eq!(tasks[1].subject, "B");
assert_eq!(tasks[1].status, TaskStatus::Inprogress);
assert_eq!(tasks[2].taskid, 3);
assert_eq!(tasks[2].subject, "C");
}
} }