Repository navigation
fix: replay no longer dispatches a second EXECUTION_END hook - #8422
Wilson0511 wants to merge 3 commits into
Conversation
Crew.replay() skips prepare_kickoff, so no EXECUTION_START dispatches for the replayed run, but its execution tail (_create_crew_output) dispatched InterceptionPoint.EXECUTION_END unconditionally. After kickoff + replay an EXECUTION_END hook fired twice for a single EXECUTION_START (observed dispatch sequence: start, end, end). Change: - src/crewai/crew.py (_create_crew_output): dispatch EXECUTION_END only when _execution_start_dispatched and not _execution_end_dispatched. The flag is still set before dispatch so an END hook that raises HookAborted cannot trigger a second (failure) dispatch upstream. Why: - prepare_kickoff documents the pairing contract: EXECUTION_END fires (once) only for executions whose EXECUTION_START actually dispatched, and _dispatch_execution_end_failure already enforces it on the failure path. replay() broke it: END hooks (finalizers, per-run aggregation) ran a second time with no START to pair against. Proof: - tests/test_crew.py::test_replay_does_not_redispatch_execution_end_hook fails on main (dispatches == [start, end, end]) and passes with the fix. Full tests/test_crew.py: 137 passed, 1 pre-existing litellm/vcr env failure (test_crew_kickoff_streaming_usage_metrics) identical on clean main. Risk: - Low. kickoff/prepare_kickoff always sets _execution_start_dispatched before _execute_tasks, so the guard is a no-op on the normal path; only replay's tail changes behavior. 🤖 Generated with ZCode (AI agent) on behalf of Wilson0511 Co-Authored-By: ZCode <[email protected]>
Strix Security ReviewWarning This pull request has 31 commits after the last Strix review ( ✅ No open security findings. PR summary: This PR makes `Crew.replay()` stop dispatching a second `EXECUTION_END` interception hook: the success-path tail now dispatches that hook only when a matching `EXECUTION_START` fired and no end hook has fired yet, mirroring the guard already used on the failure path. It adds a regression test asserting `kickoff()` followed by `replay()` fires each boundary hook exactly once. Review summaryReviewed the full diff: a single 5-line guard in Ruled out (2)
Review 1 on this PR · Reviewed by Strix |
|
Can you resolve the conflict once? |
Resolve test_crew.py conflict by keeping both sides: this branch's test_replay_does_not_redispatch_execution_end_hook and main's two replay-failure tests from crewAIInc#8418 (test_replay_failure_emits_kickoff_failed_event, test_replay_failure_does_not_leak_kickoff_scope_into_next_kickoff), plus the CrewKickoffFailedEvent import the latter needs. Verified: tests/test_crew.py -k replay -> 17 passed on the merged tree; full file 138 passed with the pre-existing local-env vcr failure in test_crew_kickoff_streaming_usage_metrics (fails identically on clean origin/main). 🤖 Generated with ZCode (AI agent) on behalf of Wilson0511 Co-Authored-By: ZCode <[email protected]>
|
Conflict resolved — merged latest |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Want higher recall? High effort reviews run extra passes and find more bugs. A team admin can switch effort levels in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit b1e4494. Configure here.
The merge of origin/main into fix/replay-execution-end-pairing kept this PR's replay tests but dropped two unrelated regression tests plus their ScriptedReActLLM helper from the main side: - test_hierarchical_crew_delegates_to_workers_on_every_kickoff - test_crew_test_setup_failure_pairs_events - _ScriptedReActLLM and the asyncio / BaseLLM / CrewTestFailedEvent imports Restore them verbatim from main. Flagged by Cursor Bugbot on the PR. Change: re-add the three blocks and imports, no source changes. Why: conflict resolution must not silently drop main's regression coverage. Proof: the five restored tests pass; -k 'replay or <restored>' is 19 passed with the 3 known OPENAI_API_KEY-env failures unchanged. Risk: none — additive test-file changes only. 🤖 Generated with ZCode (AI agent) on behalf of Wilson0511 Co-Authored-By: ZCode <[email protected]>
|
Good catch by Bugbot — the conflict resolution had indeed dropped those two regression tests (plus the |

Fixes #8421
Problem
Crew.replay()skipsprepare_kickoff, so noEXECUTION_STARTdispatches for the replayed run — but its execution tail (_create_crew_output) dispatchedInterceptionPoint.EXECUTION_ENDunconditionally. Afterkickoff()+replay(), every@on(InterceptionPoint.EXECUTION_END)hook had fired twice for a singleEXECUTION_START(observed dispatch sequence:start, end, end).This violates the pairing contract documented in
prepare_kickoff:which
_dispatch_execution_end_failurealready enforces on the failure path.Change
src/crewai/crew.py(_create_crew_output): dispatchEXECUTION_ENDonly when_execution_start_dispatched and not _execution_end_dispatched— the same guard the failure path uses. The flag is still set before dispatch so an END hook that raisesHookAbortedcannot trigger a second (failure) dispatch upstream.Verification
tests/test_crew.py::test_replay_does_not_redispatch_execution_end_hook: fails onmain(dispatches == ['start', 'end', 'end']), passes with the fix.tests/test_crew.py -k replay: 15 passed.tests/hooks/: 200 passed, 1 pre-existing failure (test_a_structured_litellm_call_is_seen_once, missing optional litellm dep — identical on cleanmain).tests/test_crew.py: 137 passed, 1 pre-existing litellm/vcr env failure identical on cleanmain.mypy src/crewai/crew.py: clean;rufferror count unchanged vsmain.Risk
Low:
kickoff()always sets_execution_start_dispatched(viaprepare_kickoff) before reaching_execute_tasks, so the guard is a no-op on the normal path; only replay's tail changes behavior.🤖 Generated with ZCode (AI agent) on behalf of @Wilson0511
Note
Low Risk
Narrows when
EXECUTION_ENDruns; standard kickoff still pairs start/end, and only replay’s extra end dispatch is skipped.Overview
Fixes duplicate
EXECUTION_ENDinterception hooks afterkickoff()+replay().replay()does not runprepare_kickoff, so it never dispatchesEXECUTION_START, but_create_crew_outputstill firedEXECUTION_ENDon every successful run—listeners sawstart, end, endfor one start.In
_create_crew_output,EXECUTION_ENDis dispatched only when_execution_start_dispatchedis true and_execution_end_dispatchedis false, matching the pairing logic already used in_dispatch_execution_end_failure. Normalkickoff()is unchanged because it always sets the start flag viaprepare_kickoff.Adds regression test
test_replay_does_not_redispatch_execution_end_hookasserting exactly one start and one end after kickoff and replay.Reviewed by Cursor Bugbot for commit e9bc5fb. Bugbot is set up for automated code reviews on this repo. Configure here.