Commit graph

8046 commits

Author SHA1 Message Date
Que0x
a4ecb3da9a fix(skills): strip UTF-8 BOM before parsing SKILL.md frontmatter
A UTF-8 BOM saved into a SKILL.md (e.g. Notepad or PowerShell `>`) is kept by
read_text(encoding="utf-8"), so the string handed to parse_frontmatter starts
with the BOM and the startswith("---") fence check fails. The whole frontmatter
is then silently dropped: the skill loads with no name/description, `platforms`
gating falls open (a macOS-only skill becomes visible everywhere), and
required_environment_variables / metadata.hermes.config setup never fires.

Strip a single leading BOM at the top of parse_frontmatter, the shared
chokepoint for every local skill-loading path (_parse_skill_file,
discover_all_skill_config_vars, DESCRIPTION.md parsing, _inject_skill_config,
and the tools/skills_tool._parse_frontmatter re-export), so the whole class is
covered, not just the reported site. Only the leading marker is removed; a BOM
mid-content is left as data. Mirrors the existing file-tools BOM handling
(#35278) and CONTRIBUTING.md "File encoding".

Adds tests: BOM'd frontmatter parses identically to plain, the body is
BOM-free, platforms gating and config-var extraction survive a BOM, and an
end-to-end BOM-write / plain-read round trip (mirroring _parse_skill_file).
2026-07-17 04:52:02 -07:00
Teknium
f28248c662 test: update lock-io auto-reset assertion to the promote write-path
test_auto_reset_does_not_recover_session_being_ended asserted the old
end_session('session_reset') call; auto-reset now writes through
promote_to_session_reset with the specific auditable reason
('suspended' for a suspended entry). Missed in the local targeted run
because the file wasn't in the touched-suite list; caught by CI shard 6.
2026-07-17 04:51:39 -07:00
Teknium
9fc0074bac fix(gateway): unify reset boundaries vs recovery — promote accidental ends, honor mode=none, adapter-aware resume guidance
Unifies the two gateway subsystems that were fighting each other: the
'never lose a session' recovery machinery (#54878 stale-route self-heal,
find_latest_gateway_session_for_peer reopening agent_close/ws_orphan_reap
rows) and the session reset/expiry machinery (expiry watcher, /new,
/resume, resume_pending freshness gate).

The unified contract:
- INTENTIONAL boundaries (expiry finalization, auto-reset, /new,
  /resume switch) are recorded durably via promote_to_session_reset(),
  which upgrades accidental recoverable end_reasons (agent_close,
  ws_orphan_reap) to the explicit boundary while preserving other
  explicit reasons (compression, etc.). Recovery then correctly refuses
  to resurrect them.
- ACCIDENTAL ends (crash, cleanup bug, mistaken reaper) stay
  recoverable — genuine crash recovery is untouched.

On top of the cherry-picked contributor commits:
- promote_to_session_reset widened to ws_orphan_reap + parameterized
  reason so auto-reset paths stay auditable (idle/daily/suspended/
  resume_pending_expired) (#61220, #61993, #63539)
- get_or_create_session auto-reset, reset_session (/new), and
  switch_session (/resume) all write through the promote path — the
  first-reason-wins end_session no-op could previously leave a reset
  session resurrectable behind a stale agent_close row (#61993)
- resume_pending freshness gate now honors session_reset.mode=none:
  explicit opt-out of automatic resets also opts out of the zombie
  gate (#61052)
- resume recovery note extracted to build_resume_recovery_note() and
  made adapter-aware via a new interactive_resume capability flag:
  webhook/api_server auto-resume turns now CONTINUE the interrupted
  task instead of emitting an unanswerable 'session restored'
  acknowledgement that abandoned the work (#57056)
- tests updated to call the real note builder instead of mirroring it

E2E-validated against a real SessionDB + SessionStore in a temp
HERMES_HOME: expiry->agent_close->no-resurrection, /new promote,
crash recovery preserved, mode=none opt-out, routing-table flag sync.
2026-07-17 04:51:39 -07:00
dsad
d17daf0b12 fix(gateway): keep stale route when recovery lookup fails 2026-07-17 04:51:39 -07:00
dsad
f5b6112226 fix(gateway): fail closed on active-process check errors 2026-07-17 04:51:39 -07:00
joelbrilliant
cecf2767ee fix(gateway): preserve lazy reset after session expiry 2026-07-17 04:51:39 -07:00
hejuntt1014
3c7bab9c65 fix(gateway): notify user and log correct end_reason for resume_pending_expired resets
When a gateway session with resume_pending=True is not recovered within the
auto-continue freshness window (e.g. because repeated API calls timed out on a
large context), get_or_create_session correctly creates a new session.  However
two gaps existed:

1. The user received no notification — resume_pending_expired fell through the
   generic "inactive for Xh" else-branch in run.py, which produces wrong wording
   and (for session_reset.mode: none users) is gated on policy.notify that
   evaluates to False.
2. The old session was ended in state.db with the hardcoded generic reason
   "session_reset", making it impossible to distinguish from a normal idle/daily
   reset in post-mortem analysis.

Fix:
- gateway/run.py: add an explicit resume_pending_expired case for the agent
  context note ("gateway restart recovery timed out") and the user-facing
  notification.  Always notify for this reason — like suspended — because the
  user had an active session that was silently replaced.
- gateway/session.py: pass auto_reset_reason as the DB end_reason instead of
  the hardcoded "session_reset", so all auto-reset paths are auditable.
- tests: extend TestResumePendingExpiredAutoReset in test_session_reset_notify.py
  with five new cases that cover the reason, activity flag, DB end_reason,
  non-regression of the idle path, and freshness-disabled bypass.

Closes #58933

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-17 04:51:39 -07:00
Alec
039f6b2f1b test(gateway): add overdue-policy guard for stale-agent-close recovery path
When the #54878 self-healing path drops a stale sessions.json entry, the
fix at gateway/session.py:1765 now checks _should_reset() before falling
through to DB recovery. This test covers the case where the stale entry's
session is overdue under an idle/daily reset policy — it must create a
fresh session, set auto-reset metadata, and NOT call reopen_session().
2026-07-17 04:51:39 -07:00
saitsuki
3305dcedbb fix: conditional promote + real SessionDB tests
Address review feedback on #63068:

1. Replace unconditional reopen_session() + end_session() with a
   conditional promote_to_session_reset() method in SessionDB.
   The new method only promotes live rows or rows ended with
   agent_close — explicit boundaries (compression, session_reset,
   new_command) are preserved via first-writer-wins semantics.

2. Rewrite tests to use real SessionDB instead of MagicMock:
   - 7 unit tests for promote_to_session_reset edge cases
   - 3 integration tests verifying the actual recovery contract
     in find_latest_gateway_session_for_peer after promotion
2026-07-17 04:51:39 -07:00
saitsuki
e701cdc86e fix(gateway): end finalized expired sessions as reset 2026-07-17 04:51:39 -07:00
Teknium
78b9d98d76 fix(codex): surface nested error envelope in Responses type=error SSE frames
Port from anomalyco/opencode#36130: the Responses spec carries streaming
error details at the top level of the error frame, but the official OpenAI
SDK and several OpenAI-compatible proxies wrap them in an HTTP-style nested
envelope ({"type": "error", "error": {code, message, param}}).

_raise_stream_error only read top-level fields, so nested-envelope frames
collapsed to the generic 'stream emitted error event' placeholder with
code=None — the error classifier never saw the provider's real failure
reason, misrouting rate-limit / context-overflow / entitlement errors into
the generic retry path.

Top-level fields keep precedence; the envelope is a fallback. Null-tolerant
for spec-compliant frames with explicit nulls.
2026-07-17 04:51:14 -07:00
Teknium
4dc2b7be0f fix(mcp): preserve concurrent OAuth manager refresh 2026-07-17 04:50:47 -07:00
Teknium
cf3ae7c59c fix(mcp): preserve live OAuth state during reauth 2026-07-17 04:50:47 -07:00
Teknium
ebd737f4d9 fix(mcp): close hosted OAuth lifecycle gaps 2026-07-17 04:50:47 -07:00
Teknium
6045529724 fix(mcp): harden hosted OAuth across profiles and clients 2026-07-17 04:50:47 -07:00
Ben Barclay
11eaa77daf fix(mcp): serialize hosted oauth reauthorization 2026-07-17 04:50:47 -07:00
Ben Barclay
b09f1ba770 fix(mcp): reject invalid dashboard oauth callbacks 2026-07-17 04:50:47 -07:00
Ben Barclay
05dea7be04 fix(mcp): complete OAuth through hosted dashboards 2026-07-17 04:50:47 -07:00
Teknium
14ea8de763 fix(agent): harden non-finite wait recovery
Only advertise finite watchdog deadlines that are still in the future, exercise the full MoA heartbeat path, and register the salvaged contributor attribution.
2026-07-17 04:46:59 -07:00
发飙的公牛
1a5d2a12d8 fix: handle infinite Codex wait deadlines 2026-07-17 04:46:59 -07:00
Teknium
6dcbcd0277
refactor(console): remove hosted-context command blocking from Hermes Console (#66144)
The dashboard console previously ran under a 'hosted' context that
blocked most commands (auth add, config set model.*, mcp add --command,
cron --script, ...) behind an allowlist + line-policy layer. With the
full Hermes CLI now built into the dashboard, that policy layer is
redundant gatekeeping: the console gets the same command surface
everywhere.

Removed:
- ConsoleContext/contexts plumbing on ConsoleCommand + engine
- EXPECTED_HOSTED_PATHS allowlist + _mark_hosted
- _enforce_hosted_line_policy + HOSTED_CONFIG_* allow/block tables
- _dashboard_console_context() and the context field on the ready frame
- hosted-context tests; context badge in HermesConsoleModal

Kept (mechanical, not policy): shell-syntax rejection, the
interactive/server command blocks (gateway, dashboard, mcp serve, ...),
mutating-command confirmations, output caps, and command timeouts.
2026-07-17 04:33:34 -07:00
Teknium
60419dfb4b fix(codex): reconcile app-server bridge with #38835, gate commentary on show_commentary
Follow-ups on top of @xxxigm's salvaged bridge (#33294):

- Remove the now-dead narrow item/started-only mapper from #38835
  (_codex_note_to_tool_progress) — the full bridge supersedes it and
  keeps the same tool-name contract; its tests are repointed at the
  bridge helpers.
- Preserve main's request_routing/approval-bypass wiring on the
  CodexAppServerSession constructor (landed after the PR was filed).
- Gate agentMessage interim delivery on display.show_commentary so the
  app-server runtime honors the same toggle as the codex_responses
  commentary channel (tool progress is unaffected).
- Add json import (bridge helpers use json.dumps) and modernize the
  wiring test's stub agent for main's usage-accounting attributes.
2026-07-17 04:32:56 -07:00
xxxigm
68d5368f38 test(codex): regression coverage for app-server event bridge (#33200)
42 tests across five suites:

* ``TestCodexItemToToolName`` / ``TestCodexItemToArgs`` /
  ``TestCodexItemToPreview`` / ``TestCodexItemCompletionPayload`` —
  pin the per-type mapping so the synthetic tool name + args the
  UI sees match what ``CodexEventProjector`` writes into messages.
* ``TestStreamDeltaDispatch`` / ``TestToolProgressDispatch`` /
  ``TestAgentMessageInterimDispatch`` — drive each Codex
  notification shape through the bridge and assert the right
  agent callback fires with the right arguments (including the
  duration / is_error / result kwargs the gateway renders).
* ``TestBridgeRobustness`` — defensive paths: non-dict
  notifications, missing params, raising callbacks (must not
  tear down the codex turn loop), and agents without callbacks
  registered (cron / gateway-less contexts).
* ``TestBridgeWiredInRuntime`` — integration guard that
  ``run_codex_app_server_turn`` actually constructs the session
  with ``on_event=<bridge>``, preventing a future refactor from
  silently regressing live progress visibility again.
2026-07-17 04:32:56 -07:00
slow4cyl
bd208a6d77 test(cron): public save_jobs()/load_jobs() post-import HERMES_HOME regression
Review follow-up: the store-internals tests proved _current_cron_store()
resolves lazily, but not that the PUBLIC job I/O honors it. This exercises
save_jobs()/load_jobs() after a late env repoint and asserts the
import-time jobs.json stays byte-identical to a planted sentinel.
2026-07-17 16:08:56 +05:30
slow4cyl
65d6bd2b9f fix(cron): patched compatibility constants take precedence over a repointed env
Review follow-up: tests that monkeypatch CRON_DIR/JOBS_FILE/OUTPUT_DIR (the
documented process-wide compatibility surface) were bypassed by the lazy
env fallback — 3 file-permission tests, the cross-process lock test, and
the heartbeat roundtrip regressed. _current_cron_store() now snapshots the
constants at import and honors any deliberate re-point of them ahead of
the env resolution, so the precedence is: use_cron_store() override >
patched constants > fresh HERMES_HOME > import defaults. Adds a test
pinning constants-beat-env; the late-env sentinel behavior is unchanged.
tests/cron: failure set byte-identical to unpatched main on this box
(the 5 regressions gone); 138 pass in the touched files.
2026-07-17 16:08:56 +05:30
slow4cyl
5c121f157f fix(cron): resolve the no-override store fallback lazily so late env repoints can't write the real jobs file
Complements ec0227b43 (context-scoped cron store): the ContextVar override
is the right tool for deliberate cross-profile scoping, but with no
override active, _current_cron_store() returned the import-time constants —
so a HERMES_HOME set AFTER cron.jobs import (the filed incident: test
fixtures patching the env too late) still read/wrote the user's real
jobs.json. The fallback now resolves the active profile home fresh via
get_hermes_home() (context-local override, then env) and scopes the store
to it; when the home is unchanged since import, the exact module-level
constants are returned as before (zero change in the common path, and they
remain the documented compatibility surface). use_cron_store() still wins.

Three tests: late env repoint scopes the store; unchanged home returns the
import-time constants identically; an active use_cron_store() override
beats the env.
2026-07-17 16:08:56 +05:30
Teknium
0f102fa4dc
feat(browser): store full snapshots on truncation; make eval denylist opt-in (#65923)
* feat(browser): store full snapshots on truncation; make eval denylist opt-in

Two harness fixes motivated by BU_Bench results where fixed-verb + lossy
observation cost Hermes heavily vs code-driven browser agents:

1. Snapshot truncation no longer loses content. When a snapshot exceeds
   the 8000-char threshold, the complete accessibility tree is saved to
   cache/web (same truncate-and-store pattern as web_extract) and the
   truncated view / LLM summary includes the file path plus a ready-made
   read_file call. Element refs beyond the cut are recoverable without
   re-snapshotting. Stored copies are force-redacted and capped at 2MB;
   content-hash filenames dedupe repeated snapshots of the same page.

2. The browser_console(expression=...) sensitive-primitive denylist is
   now opt-in via browser.restrict_evaluate (default false). The
   names-based denylist blocked legitimate DOM extraction — any selector
   or expression containing 'fetch', 'cookie', 'input', etc. — which
   crippled the agent's only programmatic page-inspection path. The
   SSRF/private-URL egress guards in _browser_eval are independent of
   this policy and remain always-on. browser.allow_unsafe_evaluate keeps
   its meaning (bypass the denylist) for configs that already set it.

* test: update None-guard test for stored-snapshot pointer in _extract_relevant_content

test_normal_content_returned pinned the exact return value; the summary
now carries a pointer to the stored full snapshot. Assert the summary
passes through and the pointer is present instead.

* feat(browser): align snapshot threshold with web_extract's 15k char budget

SNAPSHOT_SUMMARIZE_THRESHOLD 8000 -> 15000, matching
web_tools.DEFAULT_EXTRACT_CHAR_LIMIT so the snapshot and web_extract
truncate-and-store paths give the model the same per-page budget.
_truncate_snapshot's default max_chars now follows the constant.
Invariant test added; docs (en+zh) and CLI tip updated.
2026-07-16 23:41:26 -07:00
Teknium
779019ef7d feat(agent): add display.show_commentary toggle for Codex commentary channel
Commentary delivery is on by default; users who find the extra mid-turn
narration noisy can set display.show_commentary: false to restore the
previous behavior (commentary routed to the reasoning channel, visible
only with show_reasoning).

- hermes_cli/config.py: display.show_commentary default true
- agent/agent_init.py: wire config -> agent.show_commentary
- run_agent.py: gate structured commentary extraction on the flag
- agent/codex_runtime.py: gate live-stream commentary callback (falls
  back to legacy reasoning-channel routing when off)
- docs + 2 tests (interim path off, live stream fallback)

Also adds AUTHOR_MAP entries for davidrobertson and 100yenadmin.
2026-07-16 23:27:12 -07:00
Eva
7041c56cdf feat(agent): stream Codex commentary separately 2026-07-16 23:27:12 -07:00
Eva
b008131b54 fix(agent): harden Codex commentary interim delivery 2026-07-16 23:27:12 -07:00
David Robertson
a15397d61a fix(agent): redact Codex interim commentary 2026-07-16 23:27:12 -07:00
David Robertson
136ade2ed5 fix(agent): surface Codex commentary items as interim messages 2026-07-16 23:27:12 -07:00
cresslank
34837597d2 fix(auth): make xAI OAuth pools multi-account resilient
Keep each xAI OAuth auth-add login as an independent manual device-code pool entry and recognize xAI personal-team spending-limit 403 responses as billing exhaustion. Preserve the structured top-level error message so the failed credential is quarantined and the next healthy account is selected without attempting a pointless token refresh.

Route direct xAI HTTP consumers through the credential pool as well. Proactive and 401-reactive refreshes update the exact issuing manual entry, preserve validated xAI base URL overrides, and serialize single-use refresh-token rotation across concurrent pool instances.
2026-07-17 11:37:25 +05:30
Thatgfsj
ef1c622105 fix(title): prevent stale background title generation from reloading unloaded Ollama models
Add a runtime_validator callback to generate_title() / auto_title_session()
/ maybe_auto_title(). Callers snapshot the session's model+provider when
spawning the background titler; the validator runs right before the LLM
request and skips it silently when the live runtime no longer matches —
so a stale title request can't reload a model that strict_single_load
already evicted after a user model switch. Fail-open: a raising validator
never disables titling.

Wired at all four call sites (cli, gateway, tui_gateway, acp_adapter).

Surgical reapply of PR #19137 (base was 8k+ commits stale; the original
patch predates the pinned-language prompts, the atomic-write helper, and
the moved TUI/ACP call sites). Original work by @Thatgfsj. Closes #19027.
2026-07-16 23:07:13 -07:00
Steven Seagondollar
ed2f48b2e6 fix(titles): use active runtime for gateway sessions 2026-07-16 22:54:09 -07:00
Teknium
7facf63ae7 fix(title): reconcile atomic auto-title writes with collision dedup retry
Combines the two salvaged fixes so they compose instead of conflict:
_persist_session_title (#50575) now writes through set_auto_title_if_empty
(#51483) when the store provides it — the collision-dedup retry and the
manual-/title race protection apply together. Predicate failure (a manual
title landed while generation was in flight) returns None: nothing written,
no callback. Legacy stores without the atomic method keep the plain
set_session_title path, including the vanished-session RuntimeError.

Tests cover both store shapes plus the race-skip path; E2E verified against
a real SQLite SessionDB (collision -> 'Weekly Report #2', manual title
preserved, cron dedup, blank guard). AUTHOR_MAP entry for rasitakyol.
2026-07-16 22:39:47 -07:00
Trevor Gordon
9bf5822a2f fix(cron): robust session title generation (#50535, #50536, #50537) 2026-07-16 22:39:47 -07:00
Raşit Akyol
d05cd7c1ef fix(agent): make auto-title write atomic 2026-07-16 22:39:47 -07:00
Raşit Akyol
f725cf830f fix(agent): avoid overwriting manual session titles 2026-07-16 22:39:47 -07:00
Teknium
b170f522a4 test(honcho): align observer-resolution test with post-#62290 call shape
The salvaged test from PR #62982 asserted search_query=None as an explicit
kwarg; current _fetch_peer_context omits search_query when None.
2026-07-16 22:25:22 -07:00
liuhao1024
e5bebe2cad fix(plugins): rebuild Honcho client when timeout config changes
The Honcho client singleton cached the HTTP timeout at first build.
In long-lived processes (gateway, dashboard), changing the timeout
via config.yaml or HONCHO_TIMEOUT had no effect until restart.

Track the resolved timeout alongside the cached client and compare
on each get_honcho_client() call. When the timeout differs, reset
the singleton so the next call rebuilds with the new value.

Fixes #57347
2026-07-16 22:25:22 -07:00
Hermes Pi
73a4574ede fix(honcho): preserve profiles for local IP config 2026-07-16 22:25:22 -07:00
RainbowAndSun
b08a13cd33 test: add prefetch_context observer-resolution test for ai_observe_others
Verify that get_prefetch_context queries user context through the
assistant observer when _ai_observe_others is enabled, matching
the fix that routes _fetch_session_context through
_resolve_observer_target.
2026-07-16 22:25:22 -07:00
fjlaowan1983
af550a7053 fix(honcho): reject whitespace-only search/reasoning queries
Strip query before validation; add regression tests (aligns upstream #11192).

Made-with: Cursor
2026-07-16 22:25:22 -07:00
Teknium
8222b16785 fix(title): follow-ups for salvaged #37349 — lazy config import, guard ordering, config example
- Make the config imports lazy inside _auto_title_enabled(), matching the
  existing _title_language() pattern (title_generator is imported from agent
  code paths where a module-level hermes_cli import risks circularity).
- Check the enabled flag after the cheap first-exchange guard in
  maybe_auto_title so config isn't read on every turn of a long session.
- Repoint the two new tests at the real import site.
- Document the key in cli-config.yaml.example and merge the enabled flag
  into the existing title_generation block in configuration.md.
- AUTHOR_MAP entry for the contributor.
2026-07-16 22:24:45 -07:00
Danny Huang
e20c3c1c29 fix: honor disabled title generation config 2026-07-16 22:24:45 -07:00
Gille
7cb2d2cd4a
fix(auth): detect configured providers absent from registry (#66017)
* fix: detect env-var-configured providers absent from PROVIDER_REGISTRY

is_provider_explicitly_configured() only checked PROVIDER_REGISTRY (a
manually-maintained dict) for env-var names. Providers that exist solely
in the models.dev catalog — e.g. openrouter — were never recognised as
explicitly configured, so they were filtered out of the desktop model
picker even when their API key was set in .env.

Add a fallback to get_provider() (which reads the models.dev catalog)
when PROVIDER_REGISTRY returns None. Both ProviderConfig and ProviderDef
expose .auth_type and .api_key_env_vars with the same shape.

* test: keep OpenRouter provider gate assertion behavioral

* chore(release): map salvaged OpenRouter contributor

---------

Co-authored-by: zzpigpinggai <zzpigpinggai@users.noreply.github.com>
2026-07-16 22:56:45 -04:00
Gille
1f7d2be22f
fix(install): detect Git Bash Mandatory ASLR failures (#64651) 2026-07-16 22:46:46 -04:00
brooklyn!
dfb76d36d5
fix(desktop): put Hermes-managed Node on PATH for install/rebuild (#66002)
Desktop launch and the update-chain rebuild install npm deps whose child
scripts shell out to a bare `node` (e.g. electron-winstaller's
select-7z-arch.js). When launched from the desktop updater chain
(Desktop -> hermes-setup -> hermes update) the shell PATH customizations are
lost, so the install dies with `'node' is not recognized` / `node: not found`.

- cmd_gui: wrap the npm-install env with with_hermes_node_path(_nixos_build_env())
  so managed Node is prepended even on a stripped PATH — mirrors the idiom
  already used by the update deps refresh. (The original fix merged nixos_env
  on TOP of the managed env, whose full os.environ copy clobbered the managed
  PATH back to bare; wrapping fixes that merge order.)
- _cmd_update_impl: spawn the `desktop --build-only` subprocess with
  with_hermes_node_path() so the child starts with managed Node from the outset.

Regression test: the desktop install env now prepends the managed Node dir
ahead of a bare updater PATH instead of passing env=None.

Co-authored-by: F4TB0Yz <jfduarte09@gmail.com>
2026-07-16 20:45:05 -04:00
Devorun
bd00212337
fix(dashboard): drop _HERMES_GATEWAY when spawning hermes actions (#52482)
The web dashboard runs inside the gateway process, so `os.environ` carries
`_HERMES_GATEWAY=1`. `_spawn_hermes_action` spread that into the subprocess env,
so a spawned `hermes gateway restart` (dashboard "Enable webhooks", Telegram QR
apply) tripped the in-process restart-loop guard and exited 1 — the gateway
never restarted, but the dashboard reported `restart_started: true` because it
only checks that the spawn succeeded.

Scrub `_HERMES_GATEWAY` from the spawned action's env, matching what the
gateway's own restart watcher already does (gateway/run.py).

Fixes #52470. Adds a test asserting the spawned env drops the loop-guard var
while keeping HERMES_NONINTERACTIVE.
2026-07-16 19:52:53 -04:00