From f78730ab86d1e3e9ae328ece9fcd8936b458f5c3 Mon Sep 17 00:00:00 2001 From: Aari Date: Tue, 11 Aug 2026 21:44:20 +0800 Subject: [PATCH] fix(dev): exclude backend runtime state from reload (#4759) --- backend/AGENTS.md | 8 +++++++- backend/Makefile | 14 +++++++++++++- backend/README.md | 8 +++++++- backend/tests/test_gateway_runtime_cleanup.py | 12 ++++++++++++ backend/tests/test_uvicorn_reload_exclude.py | 5 +++-- 5 files changed, 42 insertions(+), 5 deletions(-) diff --git a/backend/AGENTS.md b/backend/AGENTS.md index 3738046da..68ed63091 100644 --- a/backend/AGENTS.md +++ b/backend/AGENTS.md @@ -98,7 +98,7 @@ make stop # Stop all services **Backend directory** (for backend development only): ```bash make install # Install backend dependencies -make dev # Run Gateway API with reload (port 8001) +make dev # Run Gateway API with runtime-safe reload (port 8001) make gateway # Run Gateway API only (port 8001) make test # Run offline backend tests (excludes live external-API tests) make test-live # Explicitly run live DeerFlowClient tests with real APIs @@ -108,6 +108,12 @@ make format # Format code with ruff make migrate-rev MSG="..." # Autogenerate a new alembic revision (see Schema Migrations section) ``` +The backend `make dev` target pre-creates and excludes `DEER_FLOW_HOME` +(default: `backend/.deer-flow`) and `backend/sandbox` from Uvicorn's reload +watcher. Do not replace it with a bare `uvicorn --reload`: agent tasks write +Python and other runtime files below `DEER_FLOW_HOME`, which would otherwise +restart the Gateway during an active run. + The root `detect-thread-boundaries` target statically inventories execution boundaries under `backend/app/` and `backend/packages/harness/deerflow/`. It prints a concise count by execution domain and writes the complete, versioned diff --git a/backend/Makefile b/backend/Makefile index 27cb31953..31b271954 100644 --- a/backend/Makefile +++ b/backend/Makefile @@ -1,8 +1,20 @@ +DEER_FLOW_HOME ?= $(CURDIR)/.deer-flow +DEER_FLOW_HOME := $(abspath $(DEER_FLOW_HOME)) +BACKEND_SANDBOX_HOME := $(abspath $(CURDIR)/sandbox) + install: uv sync dev: - PYTHONPATH=. PYTHONIOENCODING=utf-8 PYTHONUTF8=1 uv run uvicorn app.gateway.app:app --host 0.0.0.0 --port 8001 --reload + mkdir -p "$(DEER_FLOW_HOME)" "$(BACKEND_SANDBOX_HOME)" + PYTHONPATH=. PYTHONIOENCODING=utf-8 PYTHONUTF8=1 DEER_FLOW_HOME="$(DEER_FLOW_HOME)" uv run uvicorn app.gateway.app:app --host 0.0.0.0 --port 8001 \ + --reload \ + --reload-include='*.yaml' \ + --reload-include='.env' \ + --reload-exclude='*.pyc' \ + --reload-exclude='__pycache__' \ + --reload-exclude="$(BACKEND_SANDBOX_HOME)" \ + --reload-exclude="$(DEER_FLOW_HOME)" gateway: PYTHONPATH=. PYTHONIOENCODING=utf-8 PYTHONUTF8=1 uv run uvicorn app.gateway.app:app --host 0.0.0.0 --port 8001 diff --git a/backend/README.md b/backend/README.md index d653d6976..59a557776 100644 --- a/backend/README.md +++ b/backend/README.md @@ -414,7 +414,7 @@ If a provider is explicitly enabled but required credentials are missing, or the ```bash make install # Install dependencies -make dev # Run Gateway API + embedded agent runtime (port 8001) +make dev # Run Gateway API + embedded agent runtime with safe reload (port 8001) make gateway # Run Gateway API without reload (port 8001) make lint # Run linter (ruff) make format # Format code (ruff) @@ -422,6 +422,12 @@ make detect-blocking-io # Inventory blocking IO that may block the backend even make migrate-rev MSG="..." # Autogenerate a new alembic revision against the live ORM models ``` +`make dev` pre-creates and excludes `DEER_FLOW_HOME` (by default +`backend/.deer-flow`) and `backend/sandbox` from Uvicorn's reload watcher. Use +this target instead of a bare `uvicorn --reload`: agent tasks write Python and +other runtime files under `DEER_FLOW_HOME`, and watching that directory can +restart the Gateway during an active run. + ### Schema Migrations DeerFlow's application tables (`runs`, `threads_meta`, `feedback`, `users`, diff --git a/backend/tests/test_gateway_runtime_cleanup.py b/backend/tests/test_gateway_runtime_cleanup.py index 1ccbba11a..17ee6470a 100644 --- a/backend/tests/test_gateway_runtime_cleanup.py +++ b/backend/tests/test_gateway_runtime_cleanup.py @@ -98,6 +98,18 @@ def test_local_dev_gateway_reload_excludes_runtime_state_with_absolute_dirs(): assert "--reload-exclude='.deer-flow/'" not in serve_sh +def test_backend_make_dev_gateway_reload_excludes_runtime_state_with_absolute_dirs(): + makefile = _read("backend/Makefile") + + assert "DEER_FLOW_HOME ?= $(CURDIR)/.deer-flow" in makefile + assert "DEER_FLOW_HOME := $(abspath $(DEER_FLOW_HOME))" in makefile + assert "BACKEND_SANDBOX_HOME := $(abspath $(CURDIR)/sandbox)" in makefile + assert 'mkdir -p "$(DEER_FLOW_HOME)" "$(BACKEND_SANDBOX_HOME)"' in makefile + assert 'DEER_FLOW_HOME="$(DEER_FLOW_HOME)" uv run uvicorn' in makefile + assert '--reload-exclude="$(DEER_FLOW_HOME)"' in makefile + assert '--reload-exclude="$(BACKEND_SANDBOX_HOME)"' in makefile + + def test_backend_container_only_exposes_gateway_port(): dockerfile = _read("backend/Dockerfile") diff --git a/backend/tests/test_uvicorn_reload_exclude.py b/backend/tests/test_uvicorn_reload_exclude.py index 430a72284..9302c1c63 100644 --- a/backend/tests/test_uvicorn_reload_exclude.py +++ b/backend/tests/test_uvicorn_reload_exclude.py @@ -14,7 +14,7 @@ Two layers of coverage: * ``test_*_resolve_*`` exercises uvicorn's real ``resolve_reload_patterns`` to pin the failure mode and the fix's mechanism. * ``test_launcher_precreates_every_absolute_reload_exclude`` enforces the actual - invariant on both launchers: every absolute exclude dir is ``mkdir -p``'d + invariant on every dev launcher: every absolute exclude dir is ``mkdir -p``'d before uvicorn starts. This encodes the root cause, so any future absolute exclude that forgets its ``mkdir`` fails here. """ @@ -35,6 +35,7 @@ REPO_ROOT = Path(__file__).resolve().parents[2] LAUNCHERS = { "scripts/serve.sh": REPO_ROOT / "scripts" / "serve.sh", "docker/dev-entrypoint.sh": REPO_ROOT / "docker" / "dev-entrypoint.sh", + "backend/Makefile": REPO_ROOT / "backend" / "Makefile", } # Shell terminators / redirects that end a simple command's argument list. @@ -161,7 +162,7 @@ def test_sandbox_mkdir_precedes_uvicorn_launch(name): """ lines = LAUNCHERS[name].read_text(encoding="utf-8").splitlines() launch_idx = next((i for i, ln in enumerate(lines) if "uv run uvicorn" in ln), None) - mkdir_idx = next((i for i, ln in enumerate(lines) if re.search(r"\bmkdir\b", ln) and "sandbox" in ln), None) + mkdir_idx = next((i for i, ln in enumerate(lines) if re.search(r"\bmkdir\b", ln) and "sandbox" in ln.lower()), None) assert launch_idx is not None, f"{name}: could not locate the 'uv run uvicorn' launch line" assert mkdir_idx is not None, f"{name}: could not locate the sandbox mkdir line"