fix(profiles): ignore cron-only directory shells in profile discovery - #94840
Nicolas-Formenton wants to merge 2 commits into
Conversation
A deleted profile can be resurrected as an empty directory shell by runtime subsystems (the cron ticker recreates <profile>/cron to write ticker_heartbeat). list_profiles() and profiles_to_serve() treated any directory under profiles/ as a valid profile, so the shell reappeared in Profile Model Gateway Alias Distribution ─────────────── ─────────────────────────── ─────────── ─────────── ──────────────────── ◆default stealth/ox-alpha running — — edgezenn tencent/hy3:free stopped edgezenn — macos stealth/ox-alpha stopped macos — quant-hft stealth/ox-alpha stopped quant-hft — saf-auditor gpt-5.6-sol stopped saf-auditor — saf-money gpt-5.6-sol stopped saf-money — saf-scout gpt-5.6-luna stopped saf-scout — saf-strategist gpt-5.6-sol stopped saf-strategist — saf-writer gpt-5.6-terra stopped saf-writer — vest stealth/ox-alpha stopped — — vestcardimpla — stopped — — vestcardqa — stopped — — vestcardreview — stopped — — and was served again by a multiplexed gateway. Both discovery paths now require at least one durable identity marker (config.yaml, .env, SOUL.md, profile.yaml, auth.json, or state.db) before treating a directory as a profile. A runtime-only shell fails that check and stays invisible; a real profile is unaffected because every one of those files exists from creation onward. Adds regression tests for both list_profiles and profiles_to_serve(multiplex=True).
eddcafe to
8694b6f
Compare
This PR fixes a profile-discovery edge case where cron subsystems recreate a deleted profile's directory (e.g., for cron job state), causing an empty "ghost" directory to appear in A few concerns:
|
…contributor email - test_cron_profile_homes_follow_allowlist created its fixture profiles as bare directories; profile discovery now requires a durable identity marker, so the fixture writes an empty config.yaml (same contract a real profile satisfies). - Add contributors/emails mapping for nicolas@users.noreply.github.com so the contributor attribution check passes.
What does this PR do?
Stops runtime-only directory shells under
profiles/from being discovered asreal profiles. A deleted profile can be resurrected as an empty directory by
runtime subsystems — most visibly the cron ticker, which recreates
<profile>/cron/to writeticker_heartbeat— and that shell then reappearedin
hermes profile listand was served again by multiplexed gateways.list_profiles()andprofiles_to_serve()treated any directory underprofiles/as a valid profile. This PR requires at least one durable identitymarker before treating a directory as a profile:
config.yaml.envSOUL.mdprofile.yamlauth.jsonstate.dbEvery one of these exists from profile creation onward, so real profiles are
unaffected; a cron-only shell fails the check and stays invisible. The check is
a bounded stat scan (≤6 stats per candidate), keeping
profiles_to_serve'sdocumented cheap-scan contract intact.
Related upstream work in the same problem family: #94604 (ticker recreating
archived homes), #90141 (tombstones so logging cannot resurrect profiles),
#59554 (stale
HERMES_HOMEat startup), issue #69934 ("Ghost profiles. Profilesre-appear after deleting"). Those address other resurrection paths; none
changes what counts as a discoverable profile, which is the gap this closes.
Related Issue
Fixes the discovery half of the ghost-profile cluster (#69934); no dedicated
issue found for the cron-shell variant.
Type of Change
Changes Made
hermes_cli/profiles.py_PROFILE_IDENTITY_MARKERS+_is_profile_directory().list_profiles()andprofiles_to_serve()use it instead of bareis_dir().tests/hermes_cli/test_profiles.pytest_ignores_cron_only_profile_shells(list path).test_on_ignores_cron_only_profile_shells(serve path).How to Test
profiles/<name>/cron/ticker_heartbeatreappears.hermes profile listbefore: the deleted name shows up again.pytest tests/hermes_cli/test_profiles.py -q→ both new tests pass(2 pre-existing Windows-environment failures on this machine are identical
with and without the patch; full file: 52 passed / 6 failed patched vs
50 passed / 6 failed clean).
Checklist
Code
Documentation & Housekeeping