fix(desktop): migrate active profile preference from legacy signals on first boot - #64195
DavidMetcalfe wants to merge 6 commits into
Conversation
0c39fd8 to
d83213a
Compare
teknium1
left a comment
There was a problem hiding this comment.
Thanks for targeting a real Desktop migration gap: current main maps a missing Electron preference to default in apps/desktop/src/app/gateway/hooks/use-gateway-boot.ts:233-239.
Problems
apps/desktop/electron/main.ts:6843invokes the new migration only afterstartHermes()has calledresolveRemoteBackend(primaryProfileKey())(main.ts:6738). The remote branch returns atmain.ts:6740-6761, so remote users with noactive-profile.jsonstill resolve asdefaultand never run this migration. Move migration before the first profile-dependent remote resolution.- The PR adds a multi-rung resolver and persistence behavior without Electron tests.
apps/desktop/vitest.config.ts:13-20already discoverselectron/**/*.test.ts; please cover precedence, stale PID rejection, fallback behavior, and the remote boot path.
Suggested changes
- Initialize the preference before
primaryProfileKey()is first used instartHermes(). - Put the migration decision in a testable Electron helper and add behavior tests for its validation and fallback rungs.
Automated hermes-sweeper review.
| @@ -6734,6 +6843,7 @@ async function startHermes() { | |||
| // resolves HERMES_HOME the same way `hermes -p <name>` does on the CLI. An | |||
There was a problem hiding this comment.
startHermes() resolves resolveRemoteBackend(primaryProfileKey()) before this point and returns immediately for remote mode. With a missing preference that first lookup is default, and this migration never executes. Move initialization before the first primaryProfileKey()/remote-resolution call so remote profile overrides and the persisted preference agree.
There was a problem hiding this comment.
Thanks for the close read — both findings were legitimate, and the rework addresses them.
Finding #1 (ordering) — fixed. The migration now runs at the top of startHermes(), before connectionPromise = (async () => {…}) and therefore before resolveRemoteBackend(primaryProfileKey()) inside it. Remote-mode users with a missing preference file no longer fall through to 'default' silently — the migration seeds the file first, then primaryProfileKey() reads it. Commit bb6006baa.
Finding #2 (tests) — fixed. Pulled the decision logic into a new pure module apps/desktop/electron/profile-migration.ts with a MigrationDeps bag (following the same dep-injection shape as profile-delete-routing.ts), then added apps/desktop/electron/profile-migration.test.ts with 29 vitest cases covering precedence (legacy > single-running-gateway > heuristic), stale-PID rejection (recycled PID to a non-hermes process is dropped), malformed pid files (non-integer / zero / negative PIDs), scoring edge cases (recency floored at 0.1, size floored at MIN_SIZE, larger DB beats smaller at similar recency), and single-profile fallback (best === 'default' suppresses the write). Commits 9ebfcada9 (helpers) and a6da58e93 (tests).
Two small things the cross-vendor review (Gemini Flash + GPT-OSS, parallel) flagged that aren't in your review but were worth catching:
'default'matchedPROFILE_NAME_RE, so the original reader would have accepted a legacyactive_profilefile containingdefaultand suppressed the heuristic rung. Added an explicitname === 'default'guard inreadLegacyActiveProfile.readdirSyncwasn't in the deps bag, so the orchestrator would have calledrequire('fs').readdirSyncdirectly and made the tests depend on the host filesystem. Added it to the bag so the test fixture is hermetic.
The remote-boot ordering is verified by code review of the placement — the test suite covers the pure decision logic the function relies on, which matches the repo's existing testable-helper pattern. There's no precedent in apps/desktop/electron/ for spawning an Electron process from a vitest case (only pure-helper tests); flagging that as a follow-up if you'd rather see an integration test added, but it'd be the first of its kind in the repo.
Happy to address any further concerns.
|
Rework pushed to 1. Remote-boot ordering (BLOCKER-class) → fixed. 2. Test coverage (BLOCKER-class) → fixed. Pulled the decision logic into a new pure module Two adjacent fixes the cross-vendor review (Gemini Flash + GPT-OSS in parallel) caught:
The remote-boot ordering is verified by code review of the placement; the test suite covers the pure decision logic the orchestrator relies on. There's no precedent in Inline reply also posted under the original line-level comment. |
a6da58e to
a59f19c
Compare
|
Rebased onto current Verification on rebased branch:
Branch force-pushed ( |
a59f19c to
32f6a92
Compare
|
Antigravity review of the rebase resolution (Flash + GPT-OSS in parallel) confirmed the placement is correct. Consensus:
Both reviewers suggested one small comment polish (GPT-OSS: be explicit about which call reads which profile-state, since Re-pushed SHA: The SHOULD-FIX finding (one reviewer asked for an Electron-process-spawn integration test covering the ordering) was raised and discussed in the previous review round — no precedent in |
SummaryOne PR addresses #64160. #64195 implements first-boot migration for the missing Desktop active-profile preference, while its three-file diff does not change the separate config.yaml update path implicated in the reported model/provider and Mixture of Agents settings rewrite. Related pull requests
Suggested consolidationKeep #64195 open with a salvage path as the recorded best existing fix: retain its tested active-profile migration, narrow its closing scope to that half of #64160, and track the config.yaml preservation failure separately. There are no duplicate PRs to close in this complex. Complex graphflowchart 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
I64160(["issue #64160 (open)"])
P64195["PR #64195 (open)"]
P64195 -->|best fix| I64160
class I64160 open
class P64195 open
class P64195 best
class P64195 target
click I64160 "https://github.com/NousResearch/hermes-agent/issues/64160"
click P64195 "https://github.com/NousResearch/hermes-agent/pull/64195"
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 1 pull request and 1 issue in this complex. Each diff was read against this issue; Assessment working set: 29 kB of PR diffs, 6 kB of issue/PR text, 7 kB of discussion (5 comments), 2 verify verdicts. verdicts reflect diff content, not PR titles. Part of an automated triage batch. |
…n first boot When active-profile.json does not exist (fresh install or first boot after update), seed it from the best available signal so the Desktop launches into the user's primary profile instead of always defaulting to "default". Priority ladder: 1. Legacy ~/.hermes/active_profile (explicit CLI choice via hermes profile use) 2. Running gateway (gateway.pid with verified liveness + hermes identity check via /proc/cmdline or ps -o args= to avoid PID recycling false positives) 3. state.db heuristics — hybrid recency×size score picks the primary workspace (e.g. a 409MB coder DB beats a 28MB default DB even if touched at similar times) The stored JSON includes _migrated:true for priority 3 (heuristic guess) so the renderer can optionally surface a one-time notification. Priority 1 and 2 are higher-confidence signals and skip the flag. The migration is a no-op once active-profile.json exists, and only writes when a non-default profile is confidently identified — preserving the legacy fallback-to-default behavior for single-profile users. Fixes NousResearch#64160 (active-profile half).
Addresses teknium1's review (NousResearch#64195) — the migration decision logic should be unit-testable without Electron. Pull the ladder (legacy sticky file, running-gateway scan, state.db heuristic) into pure helpers in a new `profile-migration.ts` module, following the dep-injection pattern already established by `profile-delete-routing.ts`. The helpers take an injected `MigrationDeps` bag so tests can exercise precedence, stale-PID rejection, fallback behavior, and the single-profile case without touching `/proc`, `ps`, or the host filesystem. The default profile is explicitly rejected from the legacy rung because the regex matches `default` and accepting it would suppress the heuristic that is the whole point of the migration. The wrapper in main.ts is unchanged in behavior — the same `MigrationDeps` fields get filled in from `fs`/`path` and `isHermesProcess`. The atomic write + parent-dir-create that the wrapper performs matches `writeActiveDesktopProfile`'s semantics so the migration produces a file indistinguishable from a user-driven profile switch. No production behavior change; pure code organization. Tests added in a separate commit.
… read Addresses teknium1's review (NousResearch#64195) finding #1: the previous PR placed the migration inside the connection IIFE, AFTER `resolveRemoteBackend(primaryProfileKey())`. When the preference file was missing, `primaryProfileKey()` resolved to 'default' and the remote branch returned immediately without ever reaching the migration. Remote- mode users got no migration at all. Move the call site to the top of `startHermes()`, before the connection IIFE that reads `primaryProfileKey()`. Both remote and local branches now flow through this path before any profile-dependent resolution, so the migration runs on first boot regardless of mode. The inlined implementation is replaced with a thin wrapper that builds a `MigrationDeps` bag and delegates to `migrateActiveProfileIfMissing` from `profile-migration.ts`. No production behavior change beyond the call- site move. Tests added in a separate commit.
Addresses teknium1's review (NousResearch#64195) finding #2: the multi-rung resolver needs Electron tests covering precedence, stale-PID rejection, fallback behavior, and the remote boot path. The pure decision helpers are now covered by 29 unit tests in `profile-migration.test.ts` (vitest electron project). Coverage: - precedence: legacy > single-running-gateway > state.db heuristic - stale-PID rejection: recycled PIDs not owned by hermes are dropped - malformed pid files: JSON parse errors, non-integer PIDs, zero/negative - scoring edge cases: ancient files (recency floored at 0.1), tiny files (size floored at MIN_SIZE), larger DB beats smaller at similar recency - single-profile fallback: best === 'default' suppresses the write - no-op cases: preference file already exists, missing profiles root The remote boot path is verified by code review of the call-site move (commit preceding this one) — `migrateActiveProfileIfMissing()` now runs before `primaryProfileKey()` is first read in `startHermes()`. The pure decision logic that the orchestrator relies on is covered end- to-end below; this matches the repo's testable-helper pattern (see `profile-delete-routing.test.ts`).
Polish from antigravity review of the rebase resolution (GPT-OSS): the previous comment said "BEFORE the first primaryProfileKey() / primaryBackendIsRemote() read" but those two calls live at different points — primaryBackendIsRemote() is the very next line, primaryProfileKey() is inside the connection IIFE. Be explicit about which is where so a future reader who moves one of them knows what to preserve.
- curly: brace all single-line if statements in profile-migration.ts and profile-migration.test.ts (17 errors in CI check:lint) - perfectionist/sort-imports: node:fs builtin import before vitest external - padding-line-between-statements: blank lines after block statements - prettier: normalize formatting (fmt script style) in the three touched files All 967 electron project tests still pass.
d4eda94 to
c59b4a6
Compare
Addresses teknium1's review (#64195) — the migration decision logic should be unit-testable without Electron. Pull the ladder (legacy sticky file, running-gateway scan, state.db heuristic) into pure helpers in a new `profile-migration.ts` module, following the dep-injection pattern already established by `profile-delete-routing.ts`. The helpers take an injected `MigrationDeps` bag so tests can exercise precedence, stale-PID rejection, fallback behavior, and the single-profile case without touching `/proc`, `ps`, or the host filesystem. The default profile is explicitly rejected from the legacy rung because the regex matches `default` and accepting it would suppress the heuristic that is the whole point of the migration. The wrapper in main.ts is unchanged in behavior — the same `MigrationDeps` fields get filled in from `fs`/`path` and `isHermesProcess`. The atomic write + parent-dir-create that the wrapper performs matches `writeActiveDesktopProfile`'s semantics so the migration produces a file indistinguishable from a user-driven profile switch. No production behavior change; pure code organization. Tests added in a separate commit.
… read Addresses teknium1's review (#64195) finding #1: the previous PR placed the migration inside the connection IIFE, AFTER `resolveRemoteBackend(primaryProfileKey())`. When the preference file was missing, `primaryProfileKey()` resolved to 'default' and the remote branch returned immediately without ever reaching the migration. Remote- mode users got no migration at all. Move the call site to the top of `startHermes()`, before the connection IIFE that reads `primaryProfileKey()`. Both remote and local branches now flow through this path before any profile-dependent resolution, so the migration runs on first boot regardless of mode. The inlined implementation is replaced with a thin wrapper that builds a `MigrationDeps` bag and delegates to `migrateActiveProfileIfMissing` from `profile-migration.ts`. No production behavior change beyond the call- site move. Tests added in a separate commit.
Addresses teknium1's review (#64195) finding #2: the multi-rung resolver needs Electron tests covering precedence, stale-PID rejection, fallback behavior, and the remote boot path. The pure decision helpers are now covered by 29 unit tests in `profile-migration.test.ts` (vitest electron project). Coverage: - precedence: legacy > single-running-gateway > state.db heuristic - stale-PID rejection: recycled PIDs not owned by hermes are dropped - malformed pid files: JSON parse errors, non-integer PIDs, zero/negative - scoring edge cases: ancient files (recency floored at 0.1), tiny files (size floored at MIN_SIZE), larger DB beats smaller at similar recency - single-profile fallback: best === 'default' suppresses the write - no-op cases: preference file already exists, missing profiles root The remote boot path is verified by code review of the call-site move (commit preceding this one) — `migrateActiveProfileIfMissing()` now runs before `primaryProfileKey()` is first read in `startHermes()`. The pure decision logic that the orchestrator relies on is covered end- to-end below; this matches the repo's testable-helper pattern (see `profile-delete-routing.test.ts`).
Addresses teknium1's review (NousResearch#64195) — the migration decision logic should be unit-testable without Electron. Pull the ladder (legacy sticky file, running-gateway scan, state.db heuristic) into pure helpers in a new `profile-migration.ts` module, following the dep-injection pattern already established by `profile-delete-routing.ts`. The helpers take an injected `MigrationDeps` bag so tests can exercise precedence, stale-PID rejection, fallback behavior, and the single-profile case without touching `/proc`, `ps`, or the host filesystem. The default profile is explicitly rejected from the legacy rung because the regex matches `default` and accepting it would suppress the heuristic that is the whole point of the migration. The wrapper in main.ts is unchanged in behavior — the same `MigrationDeps` fields get filled in from `fs`/`path` and `isHermesProcess`. The atomic write + parent-dir-create that the wrapper performs matches `writeActiveDesktopProfile`'s semantics so the migration produces a file indistinguishable from a user-driven profile switch. No production behavior change; pure code organization. Tests added in a separate commit.
… read Addresses teknium1's review (NousResearch#64195) finding NousResearch#1: the previous PR placed the migration inside the connection IIFE, AFTER `resolveRemoteBackend(primaryProfileKey())`. When the preference file was missing, `primaryProfileKey()` resolved to 'default' and the remote branch returned immediately without ever reaching the migration. Remote- mode users got no migration at all. Move the call site to the top of `startHermes()`, before the connection IIFE that reads `primaryProfileKey()`. Both remote and local branches now flow through this path before any profile-dependent resolution, so the migration runs on first boot regardless of mode. The inlined implementation is replaced with a thin wrapper that builds a `MigrationDeps` bag and delegates to `migrateActiveProfileIfMissing` from `profile-migration.ts`. No production behavior change beyond the call- site move. Tests added in a separate commit.
Addresses teknium1's review (NousResearch#64195) finding NousResearch#2: the multi-rung resolver needs Electron tests covering precedence, stale-PID rejection, fallback behavior, and the remote boot path. The pure decision helpers are now covered by 29 unit tests in `profile-migration.test.ts` (vitest electron project). Coverage: - precedence: legacy > single-running-gateway > state.db heuristic - stale-PID rejection: recycled PIDs not owned by hermes are dropped - malformed pid files: JSON parse errors, non-integer PIDs, zero/negative - scoring edge cases: ancient files (recency floored at 0.1), tiny files (size floored at MIN_SIZE), larger DB beats smaller at similar recency - single-profile fallback: best === 'default' suppresses the write - no-op cases: preference file already exists, missing profiles root The remote boot path is verified by code review of the call-site move (commit preceding this one) — `migrateActiveProfileIfMissing()` now runs before `primaryProfileKey()` is first read in `startHermes()`. The pure decision logic that the orchestrator relies on is covered end- to-end below; this matches the repo's testable-helper pattern (see `profile-delete-routing.test.ts`).
Summary
Fixes #64160 (active-profile half): on first boot after a Desktop update (or fresh install), seed
active-profile.jsonfrom the best available signal so the Desktop launches into the user's primary profile instead of always defaulting to "default".What this PR does
Adds
migrateActiveProfileIfMissing()inapps/desktop/electron/main.ts, called duringstartHermes()beforereadActiveDesktopProfile(). The migration is a no-op once the file exists.Priority ladder:
~/.hermes/active_profile— explicit CLI choice viahermes profile use. Highest confidence.profiles/*/gateway.pid, validates PID liveness (kill(pid, 0)) AND process identity (command-line contains "hermes" via/procorps). Avoids false positives from stale PIDs recycled by the OS.recency × log₁₀(size)score picks the primary workspace. A 409MB database beats a 28MB one even if touched at similar times during boot/update._migratedflag: The stored JSON includes_migrated: trueso the renderer can optionally surface a one-time notification that the profile was auto-detected. Once the user interacts or switches profiles,writeActiveDesktopProfile()writes without the flag.Design decisions
process.kill(pid, 0)) to avoid the case where macOS recycles a stale PID to an unrelated process (e.g. Chrome, Slack).recencyWeight × sizeWeightwhererecencyWeight = max(0.1, 30 − daysSinceModified)andsizeWeight = log₁₀(max(1024, size)). This correctly handles the case where a massive primary database (409MB) is slightly less recently touched than a tiny secondary one (3.6MB).Notes
_migratedflag enables a future UI notification (e.g. "We restored your active profile to 'coder' after the update. [Keep] [Switch Profile]") — not implemented here, but the data is available.