Skip to content

fix(profiles): keep deleted Desktop bot profiles from reappearing - #94842

Closed
fangliquanflq wants to merge 17 commits into
NousResearch:mainfrom
fangliquanflq:fix/profiles-deleted-bot-cron-resurrection
Closed

fangliquanflq wants to merge 17 commits into
NousResearch:mainfrom
fangliquanflq:fix/profiles-deleted-bot-cron-resurrection

Conversation

@fangliquanflq

Copy link
Copy Markdown
Contributor

What does this PR do?

Deleted Desktop bot profiles could silently reappear as unusable cron-only directories, so users could not make profile deletion stick. This change makes explicit deletion durable across long-lived multiplex cron workers while allowing an explicit profile create, import, or rename to restore the name intentionally.

Symptom

After deleting a bot profile in Desktop, the profile returns within the cron ticker interval with only cron/, lock files, executions.db, output, and heartbeat markers.

Impact

Affected users cannot permanently remove bot profiles through the supported UI. The ghost rows are not functional profiles, but continue to appear in the Desktop roster.

Bug Cause

Trigger: gateway/run.py multiplex cron startup and cron/scheduler_provider.py::InProcessCronScheduler._start_multiplex

Causal chain:

  1. Gateway startup resolves profiles_to_serve() once and passes a frozen list of profile homes to the multiplex cron scheduler.
  2. Desktop deletion removes a profile directory, but the scheduler keeps iterating its stale Path.
  3. record_ticker_heartbeat() calls ensure_dirs(), recreating the deleted profile as a cron-only directory.

Why it is wrong: Profile membership is mutable for the lifetime of a gateway, but cron treated its startup snapshot as permanent authority.

Working sibling / contrast: A gateway restart rescans the profiles directory and drops the deleted profile, so the stale path exists only in the long-lived scheduler state.

Ruled out: Bot roster prewarming is not the cron-only recreator. Current Desktop deletion retires pooled profile backends before the REST delete, while the recreated tree contains only files written by cron initialization.

Fix

Profile deletion now publishes a persistent marker outside the removed directory. The authoritative profiles_to_serve() scan excludes marked names, and the gateway passes a live profile-home supplier to cron so tick and heartbeat phases refresh membership. Explicit profile creation, import, and rename clear the marker only after the new profile is materialized.

Related Issue

Closes #94823

Type of Change

  • Bug fix (non-breaking change that fixes an issue)

Changes Made

  • hermes_cli/profiles.py - persist deletion intent and clear it on explicit profile materialization.
  • gateway/run.py - provide live multiplex profile membership to cron.
  • cron/scheduler_provider.py - refresh profile homes before ticks and heartbeats.
  • tests/cron/test_scheduler_provider.py - cover deletion without resurrection and intentional recreation.
  • tests/hermes_cli/test_profiles.py - cover deletion markers, failed deletes, and explicit recreation.

How to Test

  1. Create a named bot profile with multiplex cron enabled.
  2. Delete the profile while the gateway remains running and wait across multiple ticker cycles; verify the directory stays absent.
  3. Explicitly recreate the same profile and verify cron begins ticking it again without a gateway restart.
  4. Run:
scripts/run_tests.sh tests/cron/test_scheduler_provider.py tests/gateway/test_multiplex_phase0.py -q
scripts/run_tests.sh tests/hermes_cli/test_profiles.py -k 'tombstone or rmtree_failure' -q
node --test apps/desktop/src/plugins/hermes-bots/tests/bot-delete.test.mjs

Checklist

Code

  • I've read the Contributing Guide
  • My commit messages follow Conventional Commits (fix(scope):, feat(scope):, etc.)
  • I searched for existing PRs to make sure this isn't a duplicate
  • My PR contains only changes related to this fix
  • I've run the relevant test suites: 46 Python tests and 9 Desktop bot-delete tests passed
  • I've added tests for my changes
  • I've tested on Windows 11; Phase B will verify the reported lifecycle in the real environment

Documentation & Housekeeping

  • Documentation update is N/A; behavior and lifecycle rationale are documented in code
  • Config example update is N/A; no config keys changed
  • Contributor guide update is N/A; no contributor workflow changed
  • I've considered Windows and macOS filesystem behavior
  • Tool description/schema update is N/A; no model tool changed

Screenshots / Logs

N/A - automated lifecycle tests exercise the deletion, non-resurrection, and explicit recreation contract.

@alt-glitch alt-glitch added type/bug Something isn't working comp/cron Cron scheduler and job management comp/gateway Gateway runner, session dispatch, delivery comp/cli CLI entry point, hermes_cli/, setup wizard area/profiles Multi-profile isolation, HERMES_HOME scoping P2 Medium — degraded but workaround exists sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages labels Aug 25, 2026
@alt-glitch

Copy link
Copy Markdown
Contributor

This was generated by AI during triage.

Related: #94840 prevents cron-only profile shells from being discovered, while this PR prevents a deleted profile from being recreated by stale multiplex-cron membership. The fixes are complementary and should be reviewed together.

@Finn763

Finn763 commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

@fangliquanflq Closing my duplicate #95239 in favor of your #94842 (you opened earlier). Two unique contributions offered for absorption: 1. forgetLastProfileForAllConnections() in apps/desktop/src/store/connections.ts (path B: cross-workspace localStorage cleanup; my repro showed deleted profile persisted in $lastProfileByConnection after delete from non-active workspace) 2. SQLite WAL/journal awareness in identity-marker check (LOCAL_PROFILE_IDENTITY_MARKERS should arguably also accept state.db-wal / state.db-journal as evidence of active profile). Happy to rebase and submit a smaller follow-up PR for path B if your patch doesn't already cover it. Either way, thanks for picking up the canonical.

@Finn763

Finn763 commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Follow-up after re-checking both points against current head (8dd67e2) — i.e. against what this PR actually does, rather than against my withdrawn #95239:

1. Cross-workspace $lastProfileByConnection cleanup (my "path B") — already covered here, with narrower semantics than my forgetLastProfileForAllConnections() sketch, and I think correctly so: the delete flow captures $activeConnectionId up front and calls forgetLastProfileForConnection(deletionConnectionId, name) only after deleteProfile() settles successfully, keyed to the connection that owned the delete. Same-named entries on other connections are preserved on purpose — a remote machine's same-named profile is its own store, so sweeping every connection would drop an unrelated remote boot target. Both directions are pinned by tests: connections.test.ts ("forgets a deleted profile only for the source that owned it") and index.test.tsx ("keeps the boot target when profile deletion fails"). No follow-up PR needed from me on path B.

2. SQLite WAL/journal awareness in LOCAL_PROFILE_IDENTITY_MARKERS — that constant only ever existed in my withdrawn #95239; it is not part of this PR. This approach gates resurrection with an explicit tombstone (_deleted_profile_markeris_profile_deletion_marked) instead of activity-file heuristics, so there is no marker set to extend here. (For the record: if a heuristic ever replaces the tombstone, treating state.db-wal / state.db-journal as activity evidence would indeed be correct — an open SQLite handle leaves them behind even when state.db appears checkpointed.)

Net: nothing left to absorb from #95239 — one point landed, the other is moot by design. Thanks @fangliquanflq for carrying the canonical fix.

@teknium1

Copy link
Copy Markdown
Collaborator

Heads-up, @fangliquanflq — you were the earliest PR on this cluster (before #95239, #96508, #96637, #96643), and most of it is now on main by other routes: cron mkdir sites all go through _ensure_cron_dir (fail closed for a missing named profile) via 0dc9367; deleted named profiles are tombstoned and skipped by hermes_cli/profiles.py (live_only, 7e34522 / af2dc68); and the Desktop drops a deleted profile's persisted tiles in ae6d388 (dropTilesForProfile).

Two pieces of yours are not on main, which is why this stays open rather than closing: the live profile_homes resolver (main's roster is still a startup snapshot in gateway/run.py / hermes_cli/web_server.py, so profiles created later aren't ticked until restart), and forgetLastProfileForConnection$lastProfileByConnection in apps/desktop/src/store/connections.ts is not cleared on delete, so a source can still restore into a deleted profile name. A trimmed PR carrying just those two would be welcome.

@alt-glitch alt-glitch added P3 Low — cosmetic, nice to have and removed P2 Medium — degraded but workaround exists labels Sep 10, 2026
@teknium1

Copy link
Copy Markdown
Collaborator

Thanks @fangliquanflq — this landed on main through #113377 (fix(bot-mode): deleted bot profiles stay deleted across cron ticks and updates (#95188, #9), merged as 693430d. Your commits/analysis were carried in with credit (see that PR's body and Co-authored-by trailers); the salvage rebased onto current main, widened to sibling surfaces where the review found gaps, and was live-verified in the real Electron app. Closing this one as superseded — thank you for the fix.

@teknium1 teknium1 closed this Sep 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/profiles Multi-profile isolation, HERMES_HOME scoping comp/cli CLI entry point, hermes_cli/, setup wizard comp/cron Cron scheduler and job management comp/gateway Gateway runner, session dispatch, delivery P3 Low — cosmetic, nice to have sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Desktop: deleted bot profiles re-appear silently (UI delete does not stick)

4 participants