diff --git a/core/main_terminal_parts/tools_execution.py b/core/main_terminal_parts/tools_execution.py index 7e3cb10..ebd82cb 100644 --- a/core/main_terminal_parts/tools_execution.py +++ b/core/main_terminal_parts/tools_execution.py @@ -636,9 +636,12 @@ class MainTerminalToolsExecutionMixin: def _track_shallow_versioning(self, file_path: Any) -> None: """Track a file edit for shallow versioning (conversation-scoped undo).""" conversation_id = getattr(self.context_manager, "current_conversation_id", None) + # 优先使用当前用户消息的 message_id,让文件修改归属到正确的输入节点。 + message_id = getattr(self.context_manager, "current_shallow_message_id", None) or conversation_id from utils.perf_log import perf_log perf_log("_track_shallow_versioning called", extra={ "conversation_id": conversation_id, + "message_id": message_id, "project_path": str(self.project_path), "data_dir": str(self.data_dir), "file_path": str(file_path), @@ -652,9 +655,10 @@ class MainTerminalToolsExecutionMixin: data_dir=self.data_dir, conversation_id=conversation_id, ) - manager.track_edit(file_path, conversation_id) + manager.track_edit(file_path, message_id) perf_log("_track_shallow_versioning done", extra={ "conversation_id": conversation_id, + "message_id": message_id, "file_path": str(file_path), "tracked_files_count": len(getattr(manager, "_tracked_files", set())), }) diff --git a/modules/shallow_versioning.py b/modules/shallow_versioning.py index 4dfd9c8..2d23f19 100644 --- a/modules/shallow_versioning.py +++ b/modules/shallow_versioning.py @@ -189,6 +189,14 @@ class ShallowVersioningManager: # ---------------------------- # public API # ---------------------------- + def _find_latest_snapshot_by_message_id(self, message_id: str) -> Optional[ShallowSnapshot]: + """Return the most recent snapshot whose message_id matches.""" + target = str(message_id or "") + for snapshot in reversed(self._snapshots): + if str(snapshot.message_id or "") == target: + return snapshot + return None + def track_edit(self, file_path: Path | str, message_id: str) -> None: """Track a file before it is edited; backup its current content if not already tracked.""" tracking_path = self._normalize_path(file_path) @@ -206,12 +214,13 @@ class ShallowVersioningManager: backup = self._create_backup(resolved, 1) self._tracked_files.add(tracking_path) - # Add to the most recent snapshot if one exists, otherwise create a pre-edit snapshot. + # 归属到当前 message_id 对应的最新 snapshot;不存在则新建。 + # 避免把本次输入的修改追加到上一个输入的 snapshot。 now = datetime.now().isoformat() - if self._snapshots: - latest = self._snapshots[-1] - latest.tracked_file_backups[tracking_path] = backup - self._persist_snapshot(latest) + target_snapshot = self._find_latest_snapshot_by_message_id(message_id) + if target_snapshot is not None: + target_snapshot.tracked_file_backups[tracking_path] = backup + self._persist_snapshot(target_snapshot) else: snapshot = ShallowSnapshot( message_id=message_id, @@ -256,8 +265,17 @@ class ShallowVersioningManager: tracked_file_backups=tracked_file_backups, timestamp=now, ) - self._snapshots.append(snapshot) - self._snapshot_sequence += 1 + # 如果当前 message_id 已存在 snapshot(由 track_edit 预创建),则替换它, + # 避免同一个输入节点留下多个 snapshot 导致 diff 只计算最后两次之间的变化。 + existing_index = -1 + for idx, s in enumerate(self._snapshots): + if str(s.message_id or "") == str(message_id or ""): + existing_index = idx + if existing_index >= 0: + self._snapshots[existing_index] = snapshot + else: + self._snapshots.append(snapshot) + self._snapshot_sequence += 1 self._persist_snapshot(snapshot) # Evict old snapshots while keeping the most recent MAX_SNAPSHOTS. diff --git a/server/chat_flow_task_main.py b/server/chat_flow_task_main.py index 38d3ac6..cbefa60 100644 --- a/server/chat_flow_task_main.py +++ b/server/chat_flow_task_main.py @@ -431,6 +431,14 @@ def _record_hidden_versioning_checkpoint_after_run( debug_log(f"[Versioning] 记录快照失败: {exc}") except Exception as exc: debug_log(f"[Versioning] 记录快照异常: {exc}") + finally: + # 当前输入处理结束,清除浅备份消息 ID,避免后续无消息上下文时的错误归属。 + try: + ctx = getattr(web_terminal, "context_manager", None) + if ctx is not None and hasattr(ctx, "current_shallow_message_id"): + ctx.current_shallow_message_id = None + except Exception: + pass def _persist_and_echo_preceding_notice( @@ -1010,6 +1018,13 @@ async def handle_task_with_sender( videos=videos, metadata=user_message_metadata ) + # 为浅备份 track_edit 提供当前用户消息 ID,避免文件修改被归到上一个输入。 + try: + web_terminal.context_manager.current_shallow_message_id = ( + saved_user_message.get("message_id") if isinstance(saved_user_message, dict) else None + ) + except Exception: + web_terminal.context_manager.current_shallow_message_id = None try: user_message_index = len(getattr(web_terminal.context_manager, "conversation_history", []) or []) - 1 except Exception: diff --git a/server/conversation.py b/server/conversation.py index 779a456..2421faf 100644 --- a/server/conversation.py +++ b/server/conversation.py @@ -1074,14 +1074,16 @@ def get_conversation_versioning_checkpoint_detail(conversation_id, seq: int, ter shallow_message_id = snapshot_data.get("shallow_message_id") except Exception: pass - files = row.get("files") or [] - debug_log(f"[Versioning][Detail] resolved shallow_message_id={shallow_message_id} files={files}") - if shallow_message_id and isinstance(files, list): + debug_log(f"[Versioning][Detail] resolved shallow_message_id={shallow_message_id}") + if shallow_message_id: shallow_manager = ShallowVersioningManager( project_path=workspace.project_path, data_dir=workspace.data_dir, conversation_id=normalized_id, ) + # 与列表接口保持一致:从 shallow manager 重新计算文件变更, + # 而不是只给 inputs.jsonl 中原有的 files 补 patch_lines。 + files = shallow_manager.get_file_diff_stats(str(shallow_message_id)) or [] stats = shallow_manager.get_diff_stats(str(shallow_message_id)) or {} normalized_files: List[Dict[str, Any]] = [] for file_item in files: @@ -1091,7 +1093,7 @@ def get_conversation_versioning_checkpoint_detail(conversation_id, seq: int, ter path = str(item.get("path") or "") if path: patch = shallow_manager.get_file_patch_lines(path, str(shallow_message_id)) - debug_log(f"[Versioning][Detail] path={path} patch={patch}") + debug_log(f"[Versioning][Detail] path={path} patch_lines={len(patch.get('lines') or [])}") item["patch_lines"] = patch.get("lines") or [] item["patch_truncated"] = bool(patch.get("truncated")) normalized_files.append(item)