Skip to content

fix(desktop): work profile deletion silently reverted after app restart - #72152

Closed
adurham wants to merge 3 commits into
NousResearch:mainfrom
adurham:upstream-pr/desktop-profile-deletion-hygiene
Closed

fix(desktop): work profile deletion silently reverted after app restart#72152
adurham wants to merge 3 commits into
NousResearch:mainfrom
adurham:upstream-pr/desktop-profile-deletion-hygiene

Conversation

@adurham

@adurham adurham commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

What does this PR do?

Fixes two independent gaps that leave zombie backend processes / stale UI
after a work profile is deleted.

Root Cause

  1. hermes_cli/profiles.py's backend-process scanner (used by profile delete to find and terminate the profile's running backend) required
    argv[0] to resolve to an executable literally named hermes.
    Electron's pool-backend spawn resolves the hermes console-script shim's
    path and execs it via the interpreter directly (python3 /path/to/hermes ...), so argv[0] reports as python3 and the
    scanner never matches — delete removes the profile's files but leaves
    its live backend process running, still bound to a port via uvicorn.
    This accumulates across repeated delete/recreate cycles (the original
    report on Deleted profile respawns indefinitely, accumulating zombie backend processes #52279 observed 8+ zombie processes).
  2. The desktop sidebar's ProfileRail only refreshed its cached profile
    list once, on mount, so a delete/create/rename from a different
    window or the CLI left a stale ghost entry in this window until
    something unrelated triggered a refetch.

Related work already on main (read before reviewing this)

This PR is not a duplicate of either. Verified directly: even with
both of those merged, a backend process that survives because of gap #1
above still holds a bound port — it just can no longer resurrect the
profile directory. That's real, live resource hygiene, not an
already-covered symptom. Gap #2 touches a different file (ProfileRail /
profile-switcher.tsx) than #57329's rail-refresh half (which touched the
Manage-Profiles view's own profile.ts/index.tsx) and covers a distinct
staleness path — cross-window/cross-process, not same-window
delete-then-refresh (which #57329 already handles via the shared
$profiles atom ProfileRail already subscribes to).

Changes Made

  • hermes_cli/profiles.py: _profile_bound_backend_pids() now also
    recognizes a python-interpreter argv[0] (python/python3/python3.12/
    pythonw(.exe)) exec'ing a hermes-named console-script shim via
    argv[1].
  • apps/desktop/src/app/chat/sidebar/profile-switcher.tsx: ProfileRail
    now refreshes on window focus/visibilitychange, matching the
    existing pattern used elsewhere in the sidebar (sidebar/index.tsx,
    use-background-sync.ts, star-map.tsx, use-gateway-boot.ts all use
    the same focus + visibilitychange pattern).

Related Issue

Related to (but does not duplicate) the already-closed #52279.

Type of Change

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

How to Test

  1. Create a named profile via Desktop, delete it via hermes profile delete <name> --yes from a terminal while the profile's Desktop
    backend is running.
  2. Before this fix: the backend process survives (visible via ps aux | grep hermes), still holding its bound port.
  3. After this fix: the backend process is detected and terminated as part
    of deletion.
  4. pytest tests/hermes_cli/test_profiles.py -q — 156 passed.

Checklist

Two independent bugs let a deleted profile reappear / leave orphaned
resources on next launch:

1. hermes_cli/profiles.py's backend-process scanner required argv[0] to
   resolve to an executable literally named "hermes". Electron's
   pool-backend spawn resolves the hermes console-script shim's path and
   execs it via the interpreter directly (python3 /path/to/hermes ...), so
   argv[0] reports as "python3" and the scanner never matched the running
   backend -- delete removed the profile's files but left its live backend
   process running (still bound to a port via uvicorn), which
   accumulates across repeated delete/recreate cycles.
2. The desktop sidebar's ProfileRail only refreshed its cached profile
   list once, on mount, so a delete/create/rename from another surface
   (another window, or the CLI) left a stale ghost entry until something
   unrelated triggered a refetch. Note: a delete via this window's own
   Manage-Profiles view already refreshes the shared $profiles atom
   ProfileRail subscribes to (confirmed by reading refreshProfiles() and
   handleConfirmDelete()) -- this fix only covers the cross-window/cross-
   process staleness gap, not a duplicate of the already-merged
   NousResearch#57329's Manage-Profiles rail-refresh work.

Fix 1: recognize a python-interpreter argv[0] exec'ing a hermes-named
console-script shim via argv[1]. Fix 2: refresh the profile list on window
focus/visibilitychange, matching the existing pattern used elsewhere in
the sidebar (sidebar/index.tsx, use-background-sync.ts, star-map.tsx,
use-gateway-boot.ts all use the same focus+visibilitychange pattern).

## Related work already on main

PR NousResearch#57329 (merged) fixed the *headline* symptom from issue NousResearch#52279
(deleted profile respawns) via a different, non-overlapping mechanism:
routing profile-delete through the primary backend instead of spawning a
fresh pool backend, plus a separate recreation guard in
ensure_hermes_home() (NousResearch#49435, merged) that makes a backend spawned into a
deleted profile's directory raise FileNotFoundError instead of silently
recreating it.

This PR is NOT a duplicate of that fix. Verified: even with both of those
merged, a backend process that survives because of gap #1 above still
holds a bound port via uvicorn -- it just can no longer resurrect the
profile directory. That's real resource-hygiene, not a symptom already
covered. Gap #2 touches a different file/component (ProfileRail /
profile-switcher.tsx) than NousResearch#57329's rail-refresh half (which touched the
Manage-Profiles view's own $profiles.ts / index.tsx) and covers a
distinct staleness path (cross-window/cross-process, not same-window
delete-then-refresh).

Tests: tests/hermes_cli/test_profiles.py -- 156 passed (existing +
regression coverage for the argv[0] python-interpreter detection case).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
adurham added a commit to adurham/hermes-agent that referenced this pull request Jul 26, 2026
…tems

Documents outcomes for the 6 desktop-category Bucket A items:

Submitted (4):
- PR NousResearch#72151 -- RAF-throttle blank transcript on session switch
- PR NousResearch#72152 -- profile deletion zombie backend + cross-window rail staleness
- PR NousResearch#72153 -- Nerd Font terminal fallback
- PR NousResearch#72155 -- desktop model picker hiding Anthropic

Deprioritized, needs hand-reconciliation (3): drag-to-reorder, workspace
tab close button, queued composer wrong-session delivery. All three
rejected 1+ files on git apply --check with real semantic drift (not
line-offset noise) against the fast-churning desktop session/composer
code. Flagged for revisit rather than force-applying a stale patch.

The profile-deletion and model-picker items both got real scrutiny beyond
"does it apply cleanly": profile-deletion required two rounds of external
consult after search-first turned up 2 already-merged PRs on the same
headline symptom -- verified directly (not assumed) that this fix's two
pieces are genuinely non-overlapping gaps, not stale duplicates. Model
picker's fix was confirmed to extend an already-established upstream
credential-detection pattern rather than introduce new CC-mimicry
plumbing, addressing the specific caution flagged in the original audit.

Saved verified patches to .upstream-candidates/ for reference.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@alt-glitch alt-glitch added type/bug Something isn't working P3 Low — cosmetic, nice to have comp/desktop Electron desktop app (apps/desktop/*) comp/cli CLI entry point, hermes_cli/, setup wizard labels Jul 26, 2026
… scripts

External review (Fable) caught a real false-positive widening in the
original commit: the new argv[1] script-name check reused the loose
`script_name == "hermes" or script_name.startswith("hermes")` pattern
(copy-pasted from the exe_name check above it), but argv[1] can be ANY
user-invoked python script path when argv[0] is a bare interpreter --
unlike a directly-resolved executable name, where a false match on the
substring is rare. A user's own script named e.g. "hermes-notes.py" or
"hermes-unrelated-tool" run via `python3 <script>` would be misidentified
as the console-script shim and become killable by profile delete.

Match against the actual known console-script entry points instead
(pyproject.toml [project.scripts]: hermes, hermes-agent, hermes-acp),
stripping the script's extension before comparing.

Added 2 regression tests: one confirms the false-positive case is now
rejected (fails against the pre-fix loose-match code, confirmed via a
scripted revert), the other confirms the other two real entry points
(hermes-agent, hermes-acp) still match via the shebang-exec path.

Tests: tests/hermes_cli/test_profiles.py -- 158 passed (156 previous + 2
new).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
adurham added a commit to adurham/hermes-agent that referenced this pull request Jul 26, 2026
Ran all 7 upstream PRs filed this session back through external review
with the real diffs. 2 came back with genuine, actionable findings, both
fixed and pushed as follow-up commits to the existing PRs:

- NousResearch#72054 (MCP orphan reap): silent exception swallow in the cleanup path
  now logs; added a 4th test exercising the real shutdown()/park
  machinery end-to-end rather than only faked versions.
- NousResearch#72152 (profile deletion): tightened a false-positive-prone script-name
  match to the actual known console-script entry points.

2 more findings were checked against the real code and resolved as
non-issues (not accepted at face value, not dismissed either) -- one on
NousResearch#72087 (content/blocks divergence risk -- verified architecturally safe
since blocks are what's actually replayed regardless of content's state;
image-strip gap -- verified blocks can never contain images given how
they're populated) and one on NousResearch#72151 (ref-update ordering -- verified
correct by reading the real flush function).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

@teknium1 teknium1 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for tracing this beyond the prior deletion-respawn fix. Current main still gates _profile_bound_backend_pids() on markers or an argv[0] Hermes basename at hermes_cli/profiles.py:1342-1352; the described interpreter-plus-shim argv therefore exits before the backend/profile checks. Desktop still resolves a PATH hermes command and spawns its pool backend directly with --profile <profile> serve at apps/desktop/electron/main.ts:3876-3919 and apps/desktop/electron/main.ts:8081-8113. The mount-only rail refresh also remains at apps/desktop/src/app/chat/sidebar/profile-switcher.tsx:205-209.

Problems

  • The new focus/visibility listener behavior lacks a runtime UI regression test. The desktop jsdom test project is configured in apps/desktop/vitest.config.ts:4-16; current search found no ProfileRail test.

Suggested changes

  • Add a focused test for focus, visible/hidden visibilitychange, and cleanup on unmount.

Automated hermes-sweeper review.

// used elsewhere in the sidebar (see refreshProjects/refreshProjectTree).
useEffect(() => {
void refreshActiveProfile()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add a jsdom regression test for this listener pair: focus and visible visibilitychange should refresh, hidden visibility changes should not, and unmount should remove both listeners. The desktop UI test project already supports this (apps/desktop/vitest.config.ts:4-16).

@teknium1 teknium1 added sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:risk-platform-windows Sweeper risk: may break or behave differently on native Windows sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform area/profiles Multi-profile isolation, HERMES_HOME scoping labels Jul 30, 2026
… a tested hook

Addresses review feedback from the hermes-sweeper (salvageability=high,
keep_open): "The new focus/visibility listener behavior lacks a runtime
UI regression test... no ProfileRail test."

Rendering the full ProfileRail component for this would drag in
drag-and-drop, dialogs, hotkeys, and i18n unrelated to what needs testing.
Instead, extracted the focus/visibilitychange wiring into its own
use-profile-rail-refresh-on-active hook, matching this exact directory's
own established convention (use-profile-prewarm.ts is the same shape:
a small side-effect hook pulled out of ProfileRail specifically so it's
unit-testable in isolation).

Added 6 tests covering exactly what the review asked for: refresh on
mount, refresh on window focus, refresh on visibilitychange while
visible, NO refresh on visibilitychange while hidden, listener cleanup
on unmount, and no listener accumulation across repeated mount/unmount
cycles.

Verified the tests have real teeth: simulated the exact bug this PR
originally fixed (dropped the cleanup return, leaving listeners attached
after unmount) and confirmed 4 of 6 tests correctly fail against it --
including "no accumulate listeners" showing 7 calls instead of 1, the
exact leaked-listener signature. Restored the real fix and all 6 pass.

ProfileRail itself is otherwise unchanged in behavior -- this is a pure
extraction (same effect, same dependencies, same cleanup), not a
behavior change. Full sidebar test suite: 93 passed across 12 files (up
from 87 across 11), 0 regressions. Python side unaffected: 158 passed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@adurham

adurham commented Jul 31, 2026

Copy link
Copy Markdown
Contributor Author

Added per the sweeper's review -- extracted the focus/visibilitychange wiring into its own useProfileRailRefreshOnActive hook (matching this directory's existing use-profile-prewarm.ts convention: a small side-effect hook pulled out specifically so it's testable without rendering the whole rail's drag-and-drop/dialog/i18n surface), with 6 tests covering mount, focus, visible/hidden visibilitychange, unmount cleanup, and no-listener-accumulation across remounts.

Verified the tests have real teeth: simulated the exact original bug (dropped cleanup, listeners left attached after unmount) and confirmed 4 of 6 fail against it -- including "no accumulate listeners" showing 7 calls instead of 1. Restored the fix and all 6 pass.

ProfileRail itself is behaviorally unchanged -- same effect, same deps, same cleanup, just extracted. Full sidebar suite: 93/93 passed (up from 87), Python side unaffected: 158/158.

adurham added a commit to adurham/hermes-agent that referenced this pull request Jul 31, 2026
…s on all 7 PRs

Documents the real external engagement on the 7 upstream PRs filed
2026-07-26, and the 3 substantive follow-up fixes pushed in response:

- NousResearch#72054 closed as superseded, but merged anyway via NousResearch#74139 (contributor
  CrowLoki's reconciliation with NousResearch#62026, credited via Co-authored-by).
- NousResearch#72087, NousResearch#72151, NousResearch#72152, NousResearch#72153, NousResearch#72155, NousResearch#72164 all reviewed by the
  repo's automated sweeper -- keep_open/high on all 6.
- Fixed NousResearch#72087 (payload-proportional test assertions, catching a future
  allowlist-regression risk flagged by both the sweeper and an
  independent contributor who measured it precisely on their own fork).
- Fixed NousResearch#72152 (extracted ProfileRail's focus/visibilitychange wiring
  into a tested hook, matching the directory's own established
  use-profile-prewarm.ts pattern).
- Rebased NousResearch#72155 past a real merge conflict (an unrelated upstream
  test-pruning pass removed 3 tests my diff's context touched).

All fixes verified by simulating the exact regression each review was
warning about and confirming the new tests catch it, then restoring the
real fix.

Also noted a real environment issue found this session: the `upstream`
remote's SSH URL intermittently fails to connect from this network; a
one-off HTTPS fetch into a separate ref works around it without touching
the configured remote.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@GottZ

GottZ commented Aug 3, 2026

Copy link
Copy Markdown

This was generated by AI during triage.

Summary

Three PRs address distinct layers of the ghost-profile complex: #52301 prevents Desktop from respawning a backend during deletion, #59554 prevents stale startup state from recreating a missing profile, and #72152 detects interpreter-launched profile backends while refreshing stale Desktop profile rails.

Related pull requests

Duplicates

#52301 and #72152 both relate to #52279 but are not duplicates: #52301 fixes delete-request routing and was superseded by merged #57329, whereas #72152 targets residual backend-process detection and stale rail state. #59554 addresses a separate stale-startup recreation path.

Suggested consolidation

Keep #59554 and #72152 open with salvage paths: preserve #59554's guarded handling of stale HERMES_HOME and active_profile state, and preserve #72152's narrowly validated interpreter-launched backend detection plus its now-tested rail refresh. Treat closed #52301 as the superseded reference implementation already carried by #57329; no remaining listed PR has evidence supporting closure as a duplicate of another listed PR.

Complex graph

flowchart LR
    classDef open fill:#dbeafe,stroke:#1d4ed8,color:#1e3a8a
    classDef merged fill:#dcfce7,stroke:#15803d,color:#14532d
    classDef closed fill:#e5e7eb,stroke:#6b7280,color:#1f2937
    classDef unverified fill:#f3f4f6,stroke:#9ca3af,color:#374151
    classDef best stroke-width:3px,stroke:#b45309
    classDef target stroke-width:3px,stroke:#4338ca
    I47368(["issue #47368 (closed)"])
    I50175(["issue #50175 (closed)"])
    I52279(["issue #52279 (closed)"])
    I69934(["issue #69934 (closed)"])
    P72152["PR #72152 (open)"]
    P72152 -.->|partial| I47368
    P72152 -->|best fix| I50175
    P72152 -.->|partial| I52279
    P72152 -.->|partial| I69934
    class I47368 closed
    class I50175 closed
    class I52279 closed
    class I69934 closed
    class P72152 open
    class P72152 best
    class P72152 target
    click I47368 "https://github.com/NousResearch/hermes-agent/issues/47368"
    click I50175 "https://github.com/NousResearch/hermes-agent/issues/50175"
    click I52279 "https://github.com/NousResearch/hermes-agent/issues/52279"
    click I69934 "https://github.com/NousResearch/hermes-agent/issues/69934"
    click P72152 "https://github.com/NousResearch/hermes-agent/pull/72152"
Loading

Graph: solid arrow = fixes / best fix, dashed arrow = partial or unverified (see edge label); boxed group = PRs duplicating each other; amber border = best fix; indigo border = target; gray node = closed (state tag in the node label).

Cross-PR triage: Reviewed 3 pull requests and 4 issues in this complex. Each diff was read against this issue; Assessment working set: 26 kB of PR diffs, 29 kB of issue/PR text, 18 kB of discussion (14 comments), 19 verify verdicts. verdicts reflect diff content, not PR titles. Part of an automated triage batch.

@teknium1

Copy link
Copy Markdown
Contributor

Merged via #88228 (rebase-merge, your 3 commits with authorship preserved — commit 1ed94d2 and parents on main). Thanks @adurham for tracing this past the earlier respawn fixes and for adding the hook tests per the sweeper review — the interpreter-exec'd shim gap was verified live before merge (a real python3 <shim> --profile ... serve process that main's scanner missed and your fix caught, with the decoy-script guard holding).

Closing this PR since the branch was stale against current main; all substantive work landed via the salvage PR with your commits intact.

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/desktop Electron desktop app (apps/desktop/*) P3 Low — cosmetic, nice to have sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:risk-platform-windows Sweeper risk: may break or behave differently on native Windows type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Deleted profile respawns indefinitely, accumulating zombie backend processes

4 participants