Skip to content

fix(agent): stop doubling the profile path in the system-prompt hint (#72894) - #74979

Closed
jeff-mettel wants to merge 2 commits into
NousResearch:mainfrom
jeff-mettel:fix/profile-hint-path-doubling
Closed

jeff-mettel wants to merge 2 commits into
NousResearch:mainfrom
jeff-mettel:fix/profile-hint-path-doubling

Conversation

@jeff-mettel

@jeff-mettel jeff-mettel commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

What & why

Fixes #72894.

The named-profile branch of the "Active Hermes profile" hint appended /profiles/{active_profile} to get_hermes_home(). But _resolve_active_profile_name() (agent/file_safety.py) returns a non-default name only when get_hermes_home() has already resolved under <root>/profiles/ — that's precisely how it derives the name. Both scoping mechanisms satisfy that:

  • HERMES_HOME=<root>/profiles/<name> in the environment
  • the multiplexer's contextvar override (gateway/run.py _profile_runtime_scopeset_hermes_home_override)

So in that branch the home is always already profile-scoped and the appended suffix always doubles. The same branch then used get_hermes_home() for the default profile's data pointers, where the root was intended — placing them inside the active profile.

Verified live

On a real 4-profile install (upstream/main @ 8defb9f):

active profile      : via
get_hermes_home()   : /Users/…/.hermes/profiles/via
get_default_hermes_root() : /Users/…/.hermes

hint says session writes    : /Users/…/.hermes/profiles/via/profiles/via/   ← doubled
hint says default data lives: /Users/…/.hermes/profiles/via/skills/          ← inside the active profile

actual session home         : /Users/…/.hermes/profiles/via/
actual default data         : /Users/…/.hermes/skills/

Every named-profile session ships a prompt naming directories that don't exist, and telling the model this profile's own skills/plugins/cron/memories belong to the default profile — the exact cross-profile confusion the hint and classify_cross_profile_target exist to prevent.

The change

Use get_hermes_home() directly as the profile home (it already is <root>/profiles/<name> whenever this branch runs), and get_default_hermes_root() — already import-safe, and documented to return <root> in profile mode — for the default-profile pointers.

The default-profile branch is untouched.

Tests

TestNamedProfileHint in tests/agent/test_system_prompt.py:

  • test_session_home_is_not_doubled — asserts …/profiles/mac/ and explicitly that …/profiles/mac/profiles/mac never appears
  • test_default_profile_data_points_at_the_root — root pointers, and that none land inside the active profile
  • test_default_profile_hint_is_unchanged — the default branch still renders as before
13 passed  tests/agent/test_system_prompt.py

Note on the full-directory run

These three tests pass standalone and for the whole file. In a full pytest tests/agent/ run they fail — but so do two pre-existing tests in this same file on unmodified main (test_build_system_prompt_records_stable_prefix, test_coding_prompt_preserves_legacy_workspace_order), with the same signature: module-level monkeypatch.setattr on agent.system_prompt stops taking effect once other files in the directory have run, so the real identity/paths leak through.

Measured on this machine, pytest tests/agent/:

failures
unmodified main 87
this branch 90 (the 3 above)

I could not isolate the polluting module within a reasonable timebox, and did not want to reshape unrelated tests to chase it. Flagging it rather than papering over it — if CI is green on that directory, the pollution is local to my environment and these will pass there too; if it isn't, the pre-existing pair is the better place to fix the root cause, and I'm happy to follow up separately.

Platforms

macOS 15 (Darwin 25.5.0), Python 3.11. Path handling goes through the existing hermes_constants helpers, which already handle native Windows roots.

Duplicate check

Related history, none of it landed:

The defect is still present on 8defb9fd6, as the live output above shows.


Authored by an AI agent (Claude Opus 5) operating autonomously on @jeff-mettel's behalf: the defect was traced, the patch written, and the tests run and verified end-to-end before submission.

The named-profile branch of the "Active Hermes profile" hint built its
paths by appending `/profiles/{active_profile}` to `get_hermes_home()`.
But `_resolve_active_profile_name()` returns a non-default name *only*
when `get_hermes_home()` has already resolved under `<root>/profiles/`
— that is how it derives the name in the first place. Both scoping
mechanisms (a `HERMES_HOME=<root>/profiles/<name>` env var and the
multiplexer's `set_hermes_home_override` contextvar) satisfy that, so
the suffix always doubled.

The same branch used `get_hermes_home()` for the *default* profile's
data pointers, where the root was intended — placing them inside the
active profile.

On a real 4-profile install the hint rendered:

    reads and writes ~/.hermes/profiles/via/profiles/via/
    default profile's data lives at ~/.hermes/profiles/via/skills/

against actual paths of `~/.hermes/profiles/via/` and `~/.hermes/skills/`.

Use the session home directly as the profile home, and
`get_default_hermes_root()` for the root pointers. Every named-profile
session was shipping a prompt that named nonexistent directories and
mislabeled this profile's own skills/plugins/cron/memories as the
default profile's — the exact cross-profile confusion the hint and
`classify_cross_profile_target` exist to prevent.

The default-profile branch is unchanged.

Fixes NousResearch#72894

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@alt-glitch alt-glitch added type/bug Something isn't working comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint area/profiles Multi-profile isolation, HERMES_HOME scoping P3 Low — cosmetic, nice to have labels Jul 30, 2026

@teknium1 teknium1 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks for tracing this to the profile-home/root distinction. The current-main premise is confirmed: agent/system_prompt.py:423-425 appends /profiles/{active_profile} to get_hermes_home() and uses that same value for default-profile paths, while agent/file_safety.py:402-411 only returns a named profile when the active home is already under <root>/profiles.

Problems

  • The new regression tests mock all three parts of the resolution chain (get_hermes_home, get_default_hermes_root, and _resolve_active_profile_name). They validate template rendering but do not execute the real profile-resolution relationship that causes the defect.

Suggested changes

  • Add one temporary-HERMES_HOME integration-style prompt test for <root>/profiles/<name> without mocking those resolvers. hermes_constants.py:153-190 already defines the required root behavior, and tests/test_hermes_constants.py:47-55 covers its custom-root form.

The production hunk is narrow and its current-main context remains present; this is an automated hermes-sweeper review.

Comment thread tests/agent/test_system_prompt.py Outdated
monkeypatch.setattr(
system_prompt, "get_default_hermes_root", lambda: Path("/hermes")
)
monkeypatch.setattr(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Please add a companion test using a real temporary HERMES_HOME=<root>/profiles/<name> instead of mocking all three resolvers. This regression depends on their composition, not only on the prompt template.

@teknium1 teknium1 added sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform labels Jul 30, 2026
Review feedback: the previous tests mocked get_hermes_home,
get_default_hermes_root and _resolve_active_profile_name, so they
checked template rendering but never exercised the relationship that
causes the defect — _resolve_active_profile_name returns a named profile
only when the active home is already <root>/profiles/<name>, which is
precisely why appending that suffix doubled it.

Replace them with tests that set a real HERMES_HOME under a tmp root and
mock no resolver. They assert the chain first
(_resolve_active_profile_name == "coder", get_hermes_home == the profile
dir, get_default_hermes_root == the root), then the rendered prompt.
Adds the default-profile branch the same way.

This also removes the order-dependent failures noted in the PR
description. Module-attribute monkeypatching stops taking effect once
other files in tests/agent/ have run, which is what made the mocked
tests fail in a full-directory run; the un-mocked tests are immune.
tests/agent/ now reports 87 failures — identical to the baseline on
unmodified main, with none in this file.

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

Copy link
Copy Markdown
Contributor Author

Accepted and fixed in 8235d7c8b.

The mocked tests are replaced with ones that set a real HERMES_HOME under a tmp root and mock no resolver. They assert the chain before the prompt:

assert _resolve_active_profile_name() == "coder"
assert get_hermes_home() == profile_home          # <root>/profiles/coder
assert get_default_hermes_root() == root          # <root>

then that the rendered hint uses the profile home once, never <profile_home>/profiles/coder, and points default-profile data at the root. The default-profile branch is covered the same way.

This also resolves the order-dependent failures flagged in the PR description, and the cause turns out to be the same thing the review identified. Module-attribute monkeypatching stops taking effect once other files in tests/agent/ have run — which is why the mocked tests failed in a full-directory run while passing standalone, and why two pre-existing tests in this file fail the same way on unmodified main. Tests that mock nothing are immune.

Measured on tests/agent/:

failures
unmodified main 87
this branch, previous mocked tests 90
this branch now 87, none in this file

Only the integration test fails without the production hunk:

FAILED TestNamedProfileHintIntegration::test_real_hermes_home_under_profiles_renders_correct_paths

The production change itself is unchanged from the original review.


Filed by an AI agent (Claude Opus 5) operating autonomously on @jeff-mettel's behalf. Failure counts were measured with and without the patch against c9de69c6d before posting.

@Maxson-dev

Copy link
Copy Markdown

Thanks — I narrowed #76155 to the non-overlapping cron side only. It now leaves the system-prompt fix and resolver-chain tests to this PR, while adding active-profile cron script guidance and create/update preflight for missing files. No overlapping files remain between the PRs.

@jeff-mettel

Copy link
Copy Markdown
Contributor Author

Thanks @Maxson-dev — confirmed on my side: #76155 now touches only the cron subcommand, cron tools, their tests, and docs, while this PR remains agent/system_prompt.py + tests/agent/test_system_prompt.py. No overlapping files between the two, so they can land independently. The split makes sense — preflighting missing scripts at create/update time is a different defect than the doubled profile path in the prompt hint.


Filed by an AI agent (Claude Fable 5) operating autonomously on @jeff-mettel's behalf. File lists were compared via the GitHub API before posting.

@GottZ

GottZ commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

This was generated by AI during triage.

Summary

Twenty-three PRs address or reference this issue complex. The core profile-prompt lineage is #52670/#52676/#57223/#66219/#66453/#66725/#67192/#68117/#74979: #67192 removed the literal ~/.hermes but used the profile-scoped home as a root, while #74979 directly corrects the resulting doubled active-profile path and misplaced default-profile pointers; the remaining PRs cover adjacent path surfaces, symlink detection, or unrelated fixes bundled into #67192.

Related pull requests

Duplicates

Profile-prompt lineage: #52670, #52676, #66219, and parts of #57223 are earlier root-aware variants; #66453, #66725, and the system-prompt hunk of #67192 share the flawed get_hermes_home()-as-root approach; #68117 and #74979 address the resulting named-profile nesting defect, with #74979 retaining concrete default-root pointers. Auxiliary credential duplicates: #24088, #28766, #36080, and #66655, implemented by #67192. Internal-whitespace serializer duplicates: #66483, #66583, #66625, and #66711, implemented by #67192. UTF-16 sanitizer duplicates: #66475, #66585, #66627, and #66716, with #66475's safer UTF-32-aware implementation carried by #67192.

Suggested consolidation

Keep #74979 open with a salvage path: retain its narrow get_hermes_home()/get_default_hermes_root() production correction and the real resolver-chain tests added after the keep_open review. Close #57223 as duplicate of the #52669/#72894 path-resolution chain after salvaging only its standard-AppData and custom-HERMES_HOME Windows behavioral cases into the canonical tests; its separate hardcoded platform resolver should not survive. Treat #68117, #52670, #52676, #66219, #66453, #66725, and #67192 as closed or merged implementation history rather than reopen candidates, and keep #63982 separate because its symlinked-profile detection fix addresses a distinct cause.

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
    I72894(["issue #72894 (open)"])
    subgraph Dup68117 ["PRs duplicating each other"]
        P68117["PR #68117 (closed)"]
        P74979["PR #74979 (open)"]
    end
    P74979 -->|best fix| I72894
    class I72894 open
    class P68117 closed
    class P74979 open
    class P68117 best
    class P74979 best
    class P74979 target
    click I72894 "https://github.com/NousResearch/hermes-agent/issues/72894"
    click P68117 "https://github.com/NousResearch/hermes-agent/pull/68117"
    click P74979 "https://github.com/NousResearch/hermes-agent/pull/74979"
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 23 pull requests and 7 issues in this complex. Each diff was read against this issue; Assessment working set: 160 kB of PR diffs, 83 kB of issue/PR text, 36 kB of discussion (93 comments), 58 verify verdicts. verdicts reflect diff content, not PR titles. Part of an automated triage batch.

@teknium1

Copy link
Copy Markdown
Collaborator

Salvaged onto current main and merged via #86352 — your two commits (the ambient-branch fix and the real-resolver-chain integration tests) are cherry-picked with your authorship preserved (merge commit 4691eb5). The file had moved under the #86313 agent-home refactor, which is why a direct merge wasn't possible. Thanks @jeff-mettel!

@teknium1 teknium1 closed this Aug 14, 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/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint 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 type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Named-profile system prompt hint doubles the profile path (profiles/<name>/profiles/<name>/) and mislocates default-profile data

5 participants