Skip to content

replay() pairs the stored log to the task list by position, so a changed crew silently restores outputs onto the wrong tasks #7154

Description

@vasilisnasopoulos

Description

Crew.replay() restores previously stored task outputs onto the current task list by position, and nothing checks that position i in the stored log is the same task as position i in the crew. If the task list changed between the run and the replay — which is the workflow replay exists for — outputs land on the wrong tasks and execution can resume from the wrong one, with no error.

The same file already does this correctly on the other recovery path: _restore_runtime() matches by task.id.

Steps to Reproduce

  1. Run a crew of three tasks: RESEARCH, WRITE, REVIEW.
  2. Add one task at the front, so the crew is now PLAN, RESEARCH, WRITE, REVIEW.
  3. Replay from the stored task_id of REVIEW.

Expected behavior

Either resume at REVIEW with each restored output on the task it came from, or refuse with an error because the stored log no longer corresponds to the crew.

Screenshots/Code snippets

from crewai import Agent, Task, Crew
from crewai.tasks.task_output import TaskOutput

def mk(d):
    a = Agent(role=f"r-{d}", goal="g", backstory="b", llm="gpt-4o-mini")
    return Task(description=d, expected_output="o", agent=a)

# run 1
t = [mk("RESEARCH"), mk("WRITE"), mk("REVIEW")]
c1 = Crew(agents=[x.agent for x in t], tasks=t)
c1._task_output_handler.reset()
for i, x in enumerate(t):
    c1._store_execution_log(
        x, TaskOutput(description=x.description, raw=f"OUT-{x.description}",
                      agent=x.agent.role), i)
target = c1._task_output_handler.load()[2]["task_id"]      # REVIEW

# run 2 — one task added at the front
t2 = [mk(d) for d in ["PLAN", "RESEARCH", "WRITE", "REVIEW"]]
c2 = Crew(agents=[x.agent for x in t2], tasks=t2)

def show(tasks, start_index=None, was_replayed=False, *a, **k):
    print("resumes at:", tasks[start_index].description)
    for x in tasks:
        print(f"  {x.description:9s} <- {x.output.raw if x.output else None}")
    raise SystemExit
c2._execute_tasks = show
c2.replay(task_id=target)

Output:

resumes at: WRITE
  PLAN      <- OUT-RESEARCH
  RESEARCH  <- OUT-WRITE
  WRITE     <- None
  REVIEW    <- None

The same script with the crew left unchanged resumes at REVIEW with zero misplaced outputs, so this is the changed task list rather than the harness:

unchanged crew          resumes at REVIEW   misplaced: 0
task added at front     resumes at WRITE    misplaced: 2
tasks reordered         resumes at REVIEW   misplaced: 2

The reordering row is the quieter one: it resumes at the right task by coincidence of position, while the restored context is shuffled, so the run looks normal and the result is subtly wrong.

Evidence

crew.py, in replay():

start_index = self._find_task_index(task_id, stored_outputs)   # index into the LOG
for i in range(start_index):
    stored_output = stored_outputs[i]["output"]
    ...
    self.tasks[i].output = task_output                          # applied to the CREW
return self._execute_tasks(self.tasks, start_index, True)

task_id is used only to find a position in the log; from there everything is positional.

It cannot currently validate the pairing, because ids are not stable across runs — task.py:

id: UUID4 = Field(default_factory=uuid.uuid4, frozen=True, ...)

The checkpoint path in the same file does match on identity:

# _restore_runtime()
if task.output is not None or str(task.id) not in started_task_ids:
    continue

Possible Solution

Store something stable alongside task_index — the task's description, or a user-settable task key — and on replay compare it with self.tasks[i] before assigning, raising when they disagree. That turns a silent wrong-context run into an explicit "this log does not match this crew". A user-settable stable key would additionally make replay work across edits, which is the case replay is most useful for.

Operating System

macOS (Darwin 25.5.0, arm64)

Python Version

3.12

crewAI Version

1.15.18

crewAI Tools Version

not installed

Virtual Environment

Venv

Additional context

Reported alongside a related check on other durable-execution runtimes; this one is specific to replay() and independent of #5802.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions