From 0d553f5c620e3431b39be505a0384b745d85e6bd Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sun, 30 Nov 2025 10:07:38 +0000 Subject: [PATCH] fix: preserve original task IDs after replay MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #3999 When replaying a task, the original task ID was being overwritten in storage with the new task ID from the replayed crew instance. This made it impossible to replay the same task again using the original ID. The fix removes the task_id field from the update operation during replay, so the original task ID is preserved in storage. This allows users to replay the same task multiple times using the original ID. Changes: - Modified TaskOutputStorageHandler.update() to not update task_id when was_replayed is True - Added test_replay_preserves_original_task_id_after_replay to verify the fix Co-Authored-By: João --- .../utilities/task_output_storage_handler.py | 1 - lib/crewai/tests/test_crew.py | 105 ++++++++++++++++++ 2 files changed, 105 insertions(+), 1 deletion(-) diff --git a/lib/crewai/src/crewai/utilities/task_output_storage_handler.py b/lib/crewai/src/crewai/utilities/task_output_storage_handler.py index 2259bb833..4176a67c6 100644 --- a/lib/crewai/src/crewai/utilities/task_output_storage_handler.py +++ b/lib/crewai/src/crewai/utilities/task_output_storage_handler.py @@ -42,7 +42,6 @@ class TaskOutputStorageHandler: if log.get("was_replayed", False): replayed = { - "task_id": str(log["task"].id), "expected_output": log["task"].expected_output, "output": log["output"], "was_replayed": log["was_replayed"], diff --git a/lib/crewai/tests/test_crew.py b/lib/crewai/tests/test_crew.py index 4f485f207..dd95a9efd 100644 --- a/lib/crewai/tests/test_crew.py +++ b/lib/crewai/tests/test_crew.py @@ -3511,6 +3511,111 @@ def test_replay_setup_context(): assert crew.tasks[1].prompt_context == "context raw output" +def test_replay_preserves_original_task_id_after_replay(): + """Test that original task IDs remain valid after replay. + + This test verifies the fix for issue #3999 where replaying a task would + overwrite the original task ID in storage, making it impossible to replay + the same task again using the original ID. + """ + agent = Agent(role="test_agent", backstory="Test Description", goal="Test Goal") + task1 = Task(description="First Task", expected_output="First Output", agent=agent) + task2 = Task(description="Second Task", expected_output="Second Output", agent=agent) + + # Store the original task IDs + original_task1_id = str(task1.id) + original_task2_id = str(task2.id) + + task1_output = TaskOutput( + description="First Task Output", + agent="test_agent", + raw="first task raw output", + pydantic=None, + json_dict={}, + output_format=OutputFormat.RAW, + messages=[], + ) + task2_output = TaskOutput( + description="Second Task Output", + agent="test_agent", + raw="second task raw output", + pydantic=None, + json_dict={}, + output_format=OutputFormat.RAW, + messages=[], + ) + + crew = Crew(agents=[agent], tasks=[task1, task2], process=Process.sequential) + + # Track what gets passed to storage.update + update_calls = [] + original_update = None + + def mock_storage_update(task_index, **kwargs): + update_calls.append({"task_index": task_index, "kwargs": kwargs}) + + with patch.object(Task, "execute_sync") as mock_execute_task: + mock_execute_task.return_value = task2_output + + with patch( + "crewai.utilities.task_output_storage_handler.TaskOutputStorageHandler.load", + return_value=[ + { + "task_id": original_task1_id, + "output": { + "description": task1_output.description, + "summary": task1_output.summary, + "raw": task1_output.raw, + "pydantic": task1_output.pydantic, + "json_dict": task1_output.json_dict, + "output_format": task1_output.output_format, + "agent": task1_output.agent, + "messages": [], + }, + "inputs": {}, + }, + { + "task_id": original_task2_id, + "output": { + "description": task2_output.description, + "summary": task2_output.summary, + "raw": task2_output.raw, + "pydantic": task2_output.pydantic, + "json_dict": task2_output.json_dict, + "output_format": task2_output.output_format, + "agent": task2_output.agent, + "messages": [], + }, + "inputs": {}, + }, + ], + ): + with patch( + "crewai.memory.storage.kickoff_task_outputs_storage.KickoffTaskOutputsSQLiteStorage.update" + ) as mock_update: + mock_update.side_effect = mock_storage_update + + # Replay from task2 + crew.replay(original_task2_id) + + # Verify that the update was called but did NOT include task_id + # This is the key assertion - task_id should NOT be updated during replay + assert len(update_calls) == 1 + update_kwargs = update_calls[0]["kwargs"] + + # The fix ensures task_id is NOT included in the update kwargs + # so the original task_id is preserved in storage + assert "task_id" not in update_kwargs, ( + "task_id should not be updated during replay to preserve " + "the original task ID for future replays" + ) + + # Verify other fields are still updated + assert "output" in update_kwargs + assert "was_replayed" in update_kwargs + assert update_kwargs["was_replayed"] is True + + def test_key(researcher, writer): tasks = [ Task(