Skip to content

[Bug]: cron under profile-scoped launchd gateway falls back to default ~/.hermes instead of profile HERMES_HOME #4707

@jbeausoleil

Description

@jbeausoleil

Bug Description

On macOS, a profile-scoped gateway service can be installed as a profile-specific launchd agent (for example ai.hermes.gateway-<profile>.plist) with EnvironmentVariables -> HERMES_HOME=~/.hermes/profiles/<profile>.

However, live cron executions triggered through that gateway can still resolve Hermes home as the default ~/.hermes instead of the profile-scoped Hermes home.

This causes cron jobs to observe configuration/runtime values from the default home rather than the intended profile, even though the active profile and launchd plist both point at the named profile.

Additional Context
This was discovered while validating cron-based Telegram delivery in a profile-scoped deployment.

There is a separate, already-known cron MEDIA delivery issue/fix path:

That fix addresses raw MEDIA:/... text delivery, but it does not explain the profile/home mismatch above.

Related issues:

Steps to Reproduce

  1. Create a named profile, e.g. <profile>.
  2. Ensure:
    • ~/.hermes/active_profile contains that profile name
    • ~/Library/LaunchAgents/ai.hermes.gateway-<profile>.plist exists
    • that plist contains HERMES_HOME=~/.hermes/profiles/<profile>
  3. Start the profile-scoped gateway via launchd.
  4. Create and run a one-shot cron job that prints:
    • HOME
    • HERMES_HOME
    • hermes_constants.get_hermes_home()
    • whether default config exists at ~/.hermes/config.yaml
    • whether profile config exists at ~/.hermes/profiles/<profile>/config.yaml
  5. Observe the cron output.

Expected Behavior

Cron jobs running under the profile-scoped gateway should inherit the same effective Hermes home as the gateway service:

  • get_hermes_home() should resolve to ~/.hermes/profiles/<profile>
  • config resolution should use the named profile
  • cron should therefore see the same model/provider/terminal/gateway settings as the active profile-scoped runtime

Actual Behavior

Observed live facts from cron runs in a named-profile setup:

  • HOME resolves to the operator user home
  • HERMES_HOME=
  • get_hermes_home()=~/.hermes
  • default config exists
  • named profile config also exists
  • active profile points to the named profile
  • launchd plist HERMES_HOME points to ~/.hermes/profiles/<profile>

So the cron runtime falls back to the default Hermes home despite the profile-scoped launchd service being present and active.

Affected Component

Configuration (config.yaml, .env, hermes setup)

Messaging Platform (if gateway-related)

Telegram

Operating System

macOS

Python Version

3.11.x

Hermes Version

2.x

Relevant Logs / Traceback

**Relevant Logs / Traceback**
No single Python traceback was consistently surfaced to the chat. The most relevant observed runtime facts were:


HERMES_HOME=
GET_HERMES_HOME=~/.hermes
WRAP_RESPONSE=True
ACTIVE_PROFILE_VALUE=<profile>
PLIST_HERMES_HOME=~/.hermes/profiles/<profile>


Sanitized host-side facts:
- operator user home is the active `HOME`
- `ACTIVE_PROFILE_VALUE=<profile>`
- `PLIST_EXISTS=yes`
- `PLIST_HERMES_HOME=~/.hermes/profiles/<profile>`
- profile-scoped `launchctl print` succeeds
- separate cron facts run reported:
  - `HERMES_HOME=`
  - `GET_HERMES_HOME=~/.hermes`
  - `WRAP_RESPONSE=True`

Root Cause Analysis (optional)

This appears to be a stale or incorrectly scoped HERMES_HOME resolution inside the cron subsystem, not just a user/config misunderstanding.

Most relevant code paths:

  1. hermes_constants.get_hermes_home()

    • single source of truth:
    • return Path(os.getenv("HERMES_HOME", Path.home() / ".hermes"))
    • if HERMES_HOME is missing, Hermes falls back to the default home.
  2. hermes_cli.main._apply_profile_override()

    • correctly sets HERMES_HOME early for CLI -p/--profile runs.
    • this part of profile selection looks correct.
  3. hermes_cli.gateway.generate_launchd_plist()

    • generates a profile-scoped launchd plist with:
    • EnvironmentVariables -> HERMES_HOME=~/.hermes/profiles/<profile>
    • so the service definition itself appears correct.
  4. cron/scheduler.py

    • module-level initialization captures Hermes home once:
    • line 47: _hermes_home = get_hermes_home()
    • later job execution uses that cached module-global value:
      • lines 352-354: loads dotenv from _hermes_home / ".env"
      • lines 369-372: loads config from _hermes_home / "config.yaml"
      • lines 394-396: resolves relative prefill files under _hermes_home
  5. cron/jobs.py

    • also caches Hermes home at import time:
    • line 34: HERMES_DIR = get_hermes_home()
    • then derives cron storage paths (jobs.json, output dir) from that cached value.

Why this is likely the bug:

  • The live launchd/profile evidence says the profile-scoped service exists and embeds the profile-specific HERMES_HOME.
  • But the observed cron run still resolved get_hermes_home() to the default ~/.hermes.
  • The cron subsystem relies on module-level cached home/path globals in both cron/scheduler.py and cron/jobs.py.
  • If those modules are imported before the intended profile environment is fully in effect, or are imported/reused in a context that does not have the profile-scoped HERMES_HOME, cron will continue using stale default-home paths for dotenv/config/job storage.

In other words:

  • profile selection logic is dynamic,
  • but cron path resolution is partially frozen at import time.

That mismatch is the most plausible root cause of why a profile-scoped gateway can still produce cron runs that behave as if they are attached to the default Hermes home.

Separate but related issue:

Proposed Fix (optional)

No response

Are you willing to submit a PR for this?

  • I'd like to fix this myself and submit a PR

Metadata

Metadata

Assignees

No one assigned

    Labels

    type/bugSomething isn't working

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions