Opus finding
Race condition: /api/graph/build mutates Project state from background thread without locking
- backend/app/api/graph.py:297-333
- backend/app/api/graph.py:360-380
- backend/app/models/project.py:207-215
The build_graph endpoint spawns a daemon thread that mutates `project` (status, graph_id, error) and calls ProjectManager.save_project at multiple checkpoints. A concurrent request — e.g. another /build with force=True, or any handler that calls get_project then save_project — will read and re-write the same project.json without coordination. save_project does a non-atomic open('w')+json.dump (no tmpfile+rename), so a crash mid-write or a concurrent writer can leave a half-written / empty project.json, which subsequent get_project calls open via json.load and will raise JSONDecodeError unhandled, returning a 500 to the user. Also the in-thread `project` object captured by closure can diverge from the on-disk state if another caller saved a newer version in between.
Recommendation
(a) Make save_project atomic by writing to a tempfile and os.replace()ing into place. (b) Reload the project inside the background task before each mutation, or take a per-project lock. (c) Wrap get_project's json.load in a try/except returning None on corrupt files.