fix(hosted-rooms): local_profiles skips the profile-delete tombstone dir

`hermes profile delete` leaves `profiles/.deleted/<name>` behind.
`HostedRoomService.local_profiles()` fed every subdirectory name to the
roster, so `.deleted` failed `validate_roster`'s identifier check and
`plan_next_task` raised on every cycle for every room until the
directory was removed by hand (#106847, bug 2). Skip dot-dirs and
tombstoned profiles, using the same `named_profile_is_deleted` predicate
`hermes_cli.profiles` uses to list live profiles.

Refs #106847
This commit is contained in:
Teknium
2026-09-11 02:07:13 -07:00
parent c9b71ebaf7
commit cfeccbdf34
2 changed files with 23 additions and 1 deletions
@@ -2073,3 +2073,19 @@ def test_peer_recovery_replays_the_same_execution_generation(tmp_path: Path):
assert recovered["task_id"] == "task-1"
assert recovered["execution_generation"] == 1
assert recovered["prompt"] == "Recover the accepted review."
def test_local_profiles_skips_delete_tombstones_and_dot_dirs(tmp_path: Path):
"""`hermes profile delete` leaves ``profiles/.deleted/<name>``; neither the tombstone dir nor a
tombstoned profile is a roster member (#106847: ``.deleted`` failed validate_roster every cycle)."""
from hermes_constants import mark_named_profile_deleted
profiles = tmp_path / "profiles"
(profiles / "ops").mkdir(parents=True)
(profiles / "gone").mkdir()
mark_named_profile_deleted(profiles / "gone")
assert (profiles / ".deleted").is_dir()
service = HostedRoomService(_server(), db_path=tmp_path / "shared-state.db")
assert service.local_profiles() == ("default", "ops")
+7 -1
View File
@@ -124,9 +124,15 @@ class HostedRoomService:
return self.db_path.parent
def local_profiles(self) -> tuple[str, ...]:
from hermes_constants import named_profile_is_deleted
profiles, profiles_dir = {"default"}, self.root / "profiles"
if profiles_dir.is_dir():
profiles.update(path.name for path in profiles_dir.iterdir() if path.is_dir())
# ``profiles/.deleted/`` is the tombstone dir `hermes profile delete` leaves behind, not a
# profile: feeding it to validate_roster failed plan_next_task on every cycle (#106847).
profiles.update(
path.name for path in profiles_dir.iterdir()
if path.is_dir() and not path.name.startswith(".") and not named_profile_is_deleted(path))
return tuple(sorted(profiles))
def bindings(self) -> tuple[HostedRoomBinding, ...]: