Sprint Item
is-7 of 13 — Medium severity — land first Wednesday (PR 1 of 4)
Block Relations
|
|
| Blocked by |
— |
| Blocks |
is-9 (soft) — if this merges first, its entry goes into the same [0.2.1] changelog section rather than needing an amendment |
| Repo/day |
Independent of is-1–is-6 (local-ci, Mon–Tue) and is-11–is-13 (CCB, Thu) |
Calendar Day
Wednesday, July 29, 2026 (PR 1 of 4) — the fix lands before the release that carries it.
Planned Effort
3 story points (Medium) — sprint item is-7
Problem
Eval Test 13 (Thread Safety) flags services/summary_cache.py as the concurrency gap.
fingerprint_workspace_storage (lines 68-107) reads filesystem mtimes via _file_mtime_ns and a ThreadPoolExecutor without any module-level lock, and every cache accessor performs the fingerprint compare and the disk write as separate, unguarded steps.
list_workspace_projects (services/workspace_listing.py lines 77-99) is the clearest case — three unguarded steps:
- compute a fingerprint
- call
get_cached_projects
- rebuild on miss, then call
set_cached_projects
The same pattern repeats in:
services/workspace_tabs.py (lines 535-551)
services/workspace_db.py (lines 448-461)
services/workspace_context.py (lines 191-220)
Under the multi-threaded WSGI deployment documented in DEPLOYMENT.md, two concurrent requests can compute different fingerprints for the same logical storage state, and one thread can serve or persist a summary keyed to a stale fingerprint.
This is a live bug, which is why it is scheduled this week while two higher-severity structural findings on this same repository are deferred past July.
Goal
One PR serialising the fingerprint compute → cache read-compare → cache write sequence under a single module-level lock, for all four cache types, with a concurrency regression test.
Scope
Part A — the lock
- Add
_summary_cache_lock = threading.Lock() in services/summary_cache.py, following the pattern in utils/workspace_path.py lines 15-18.
Part B — prefer helpers over scattered acquires
- Wrap the read-compare-write sequence in new helper functions such as
get_or_build_cached_projects, rather than sprinkling acquire/release calls across the four call sites.
- Guard
get_cached_*, set_cached_*, _read_cache_file and _write_cache_file (lines 128-153).
Part C — what the race actually is
_write_cache_file already uses atomic temp-and-replace. The race is fingerprint staleness between compute and compare, not the write. Do not re-solve the write.
Part D — coverage of all four cache types
- projects
- composer map
- invalid-workspace aliases
- tab summaries
Part E — regression test
tests/test_summary_cache_concurrency.py exercising concurrent fingerprint and cache access.
Out of scope
- The search-pipeline duplication (~70% structural, Medium-High) — deferred past July. It is frozen behind
search_index.py importing three private underscore-prefixed helpers out of search.py and must be unpicked in the right order. Do not start it here.
assign_composer_workspace's nine keyword-only parameters (Medium-High) — also deferred; API shape, not a defect.
Acceptance Criteria
Verification
# from the cppa-cursor-browser checkout root
.\.venv\Scripts\Activate.ps1
pytest -q tests/test_summary_cache_concurrency.py
pytest -q
- Confirm the new test fails against the pre-fix code — a concurrency test that passes either way proves nothing.
- Run the app under a threaded WSGI server, hit the workspace listing from several clients while mutating workspace storage, and confirm no stale summary is served.
References
- Sprint: Week 31 (Jul 27–31, 2026) — Chen item is-7 (3 pt, Medium)
- Week 31 weekly plan — cppa-cursor-browser
- Week 31 issue descriptions — cppa-cursor-browser, summary-cache fingerprint lock
- Eval finding: Test 13, Thread Safety
- Files:
services/summary_cache.py:68-107,128-153; services/workspace_listing.py:77-99; services/workspace_tabs.py:535-551; services/workspace_db.py:448-461; services/workspace_context.py:191-220; utils/workspace_path.py:15-18; DEPLOYMENT.md
- Companion: Wednesday is-8 release workflow, is-9 changelog backfill, is-10 v0.2.1 tag
- Success definition: "SRI and CSP and proven-safe rendering, typed boundary, programmatic error codes, multi-worker honesty" — v0.2.1
Sprint Item
is-7 of 13 — Medium severity — land first Wednesday (PR 1 of 4)
Block Relations
[0.2.1]changelog section rather than needing an amendmentCalendar Day
Wednesday, July 29, 2026 (PR 1 of 4) — the fix lands before the release that carries it.
Planned Effort
3 story points (Medium) — sprint item is-7
Problem
Eval Test 13 (Thread Safety) flags
services/summary_cache.pyas the concurrency gap.fingerprint_workspace_storage(lines 68-107) reads filesystem mtimes via_file_mtime_nsand aThreadPoolExecutorwithout any module-level lock, and every cache accessor performs the fingerprint compare and the disk write as separate, unguarded steps.list_workspace_projects(services/workspace_listing.pylines 77-99) is the clearest case — three unguarded steps:get_cached_projectsset_cached_projectsThe same pattern repeats in:
services/workspace_tabs.py(lines 535-551)services/workspace_db.py(lines 448-461)services/workspace_context.py(lines 191-220)Under the multi-threaded WSGI deployment documented in
DEPLOYMENT.md, two concurrent requests can compute different fingerprints for the same logical storage state, and one thread can serve or persist a summary keyed to a stale fingerprint.This is a live bug, which is why it is scheduled this week while two higher-severity structural findings on this same repository are deferred past July.
Goal
One PR serialising the fingerprint compute → cache read-compare → cache write sequence under a single module-level lock, for all four cache types, with a concurrency regression test.
Scope
Part A — the lock
_summary_cache_lock = threading.Lock()inservices/summary_cache.py, following the pattern inutils/workspace_path.pylines 15-18.Part B — prefer helpers over scattered acquires
get_or_build_cached_projects, rather than sprinkling acquire/release calls across the four call sites.get_cached_*,set_cached_*,_read_cache_fileand_write_cache_file(lines 128-153).Part C — what the race actually is
_write_cache_filealready uses atomic temp-and-replace. The race is fingerprint staleness between compute and compare, not the write. Do not re-solve the write.Part D — coverage of all four cache types
Part E — regression test
tests/test_summary_cache_concurrency.pyexercising concurrent fingerprint and cache access.Out of scope
search_index.pyimporting three private underscore-prefixed helpers out ofsearch.pyand must be unpicked in the right order. Do not start it here.assign_composer_workspace's nine keyword-only parameters (Medium-High) — also deferred; API shape, not a defect.Acceptance Criteria
services/summary_cache.pyserialises fingerprint computation, cache read-compare, and cache write for each cache type: projects, composer map, invalid-workspace aliases, and tab summaries.tests/test_summary_cache_concurrency.py, exercises concurrent fingerprint and cache access and passes in CI.Verification
References
services/summary_cache.py:68-107,128-153;services/workspace_listing.py:77-99;services/workspace_tabs.py:535-551;services/workspace_db.py:448-461;services/workspace_context.py:191-220;utils/workspace_path.py:15-18;DEPLOYMENT.md