35af06c009
The all-profiles sidebar polls GET /api/profiles/projects/tree, the one heavy sidebar endpoint that was not wrapped in @_sidebar_singleflight_cache. Every poll fanned out list_profiles() + _build_project_tree() over every profile (51 on this box, ~17k SKILL.md files walked), and each profile resolution re-parsed its config.yaml because read_user_config_raw() ran uncached. On a 2-vCore VPS running 'hermes serve' for the Desktop remote backend this pinned both cores (py-spy: 110-116% sustained, HostHighCPU). - Wrap get_profiles_projects_tree in the existing single-flight cache, matching get_profiles_sessions_sidebar (5s TTL, errors[] not cached). - Memoize read_user_config_raw() on a (st_dev, st_ino, st_size, st_mtime_ns) fingerprint under _CONFIG_LOCK, same strategy as read_raw_config(). Hits return a deepcopy so write-back round-trips keep their fresh-dict semantics; parse errors are never cached; the docstring no longer claims 'no caching'. - Tests: memo semantics (deepcopy isolation, inode-replace reparse, error non-caching, parse-count), a wrapper-presence pin for both heavy sidebar endpoints, and a cold-cache autouse fixture in the scope tests (the 5s TTL otherwise leaks one test's payload into the next). Measured on the affected box: list_profiles 1.9s -> 0.16s warm, serve CPU 112% -> 22-34%, host CPU 50-70% -> ~26%.