CI mirror for upstream PR #47533 - #1
Conversation
|
Retriggering after enabling fork Actions workflow discovery. |
🔎 Lint report:
|
…swer-visible fix(desktop): keep answered clarify Q&A visible in the transcript
ShellFileOperations builds bash commands (wc/head/sed/cat/tee ...) with the target path as an argument. On a Windows/Git-Bash host a native `C:\...` path has its backslashes eaten by bash (and mangled by the msys runtime even when single-quoted) — the "Directory \drivers\etc does not exist; exiting — update your msys package" class of failures. Rewrite a native drive path to forward slashes in `_escape_shell_arg`, reusing the env layer's `_windows_to_msys_path`. Both `C:/...` and `/c/...` fix the backslash bug (the MSYS coreutils resolve either via the POSIX API). We emit `/c/...` purely for consistency: it's the same form `_windows_to_msys_path` already produces for the terminal `cd` (LocalEnvironment._quote_cwd_for_cd), so shell file ops and `cd` share one helper and one path form. Scoped from NousResearch#55481, which also patched BaseEnvironment._quote_cwd_for_cd — but LocalEnvironment already overrides that through `_windows_to_msys_path`, so on a real Windows host the base branch never ran (the cwd is already `/c/...`). Co-authored-by: konsisumer <der@konsi.org>
…ticated An aggregator whose pooled credentials are all exhausted/dead still counted as an authenticated provider during no-provider /model resolution. It then won the model-name match, was set as the sticky session provider, and poisoned every later switch with "empty API key" errors while still routing through the dead aggregator. list_authenticated_providers now requires a pool to have at least one available entry (has_available, not has_credentials / bare key presence) at all three credential-pool gates. Simple token-style entries that don't parse into exhaustion-tracked entries keep the prior behaviour, so providers whose creds live only in the auth-store credential_pool still appear. Fixes NousResearch#45759
…63048) Retain the provider-boundary core of NousResearch#52799 while reusing the pool reload and handoff paths already landed in NousResearch#53591 and NousResearch#62417. Co-authored-by: Flownium <157689911+itsflownium@users.noreply.github.com>
MoA was internally inconsistent: preset-level ops (set default / add / delete) persisted on click, but reference-model and aggregator slot edits sat behind a manual Save button. Debounce-persist slot/aggregator edits like the rest of settings and drop the redundant button, so MoA is uniformly autosave.
…ousResearch#57503) list_authenticated_providers() emits picker rows for every slug in PROVIDER_TO_MODELS_DEV that has any credential env-var set. Several of those slugs (notably 'mistral') have no PROVIDER_REGISTRY entry, so resolve_provider() rejects them as 'Unknown provider' once the user selects a model — leaving the picker showing rows that cannot actually be selected. Add a resolve-gate in section 1: if PROVIDER_REGISTRY.get(hermes_id) is None, skip the slug. The picker now only lists providers that can actually be switched to at runtime. This automatically resolves the duplicate-Mistral dedup symptom too: once the broken-from-models.dev row is filtered, the conflict between PROVIDER_TO_MODELS_DEV['mistral'] and a custom_providers 'Mistral' row is moot. Composes with NousResearch#50289 (which promotes mistral to first-class via the provider-plugin path): when that lands, PROVIDER_REGISTRY gains a 'mistral' entry and the gate becomes a no-op for it. No conflict. Tests (regression suite): - tests/hermes_cli/test_model_switch_filter_unresolved.py (new, 4 tests): Picker excludes 'mistral' when MISTRAL_API_KEY is set; 'deepseek' and 'xai' (PROVIDER_REGISTRY-backed) still appear; 'mistral' stays excluded when no key is set. Confirmed by reverting the fix and seeing the test fail with 'mistral leaked into /model picker'. Cross-checked against the existing 51 test_model_switch_* and test_custom_provider_* cases — 55/55 PASS, no regressions.
) Preserve the root cause and precedence direction from NousResearch#43538 while applying the merge before truncation and covering all declared model shapes. Co-authored-by: liuhao1024 <sunsky.lau@gmail.com>
…utosave-audit fix(desktop): autosave Mixture-of-Agents preset edits
…481-native-msys fix(windows): normalize native paths before bash file ops (supersedes NousResearch#55481)
…3058) Unify the named-provider fixes from NousResearch#52506, NousResearch#57185, NousResearch#60337, and NousResearch#60901 at the main-model normalization chokepoint. Co-authored-by: izumi0uu <izumi0uu@gmail.com> Co-authored-by: liuhao1024 <sunsky.lau@gmail.com> Co-authored-by: Paulo Henrique <paulohenrique_789@hotmail.com>
…verlay-expiry fix(tui): dismiss expired sensitive prompts
…ialog-dismiss-59765 fix(desktop): dismiss stale prompt overlays
Settings → Model rendered `fallback_providers` (a list of `{provider,
model}` objects) through the generic `list` config field, which does
`value.join(', ')` and stringified each entry to `[object Object],
[object Object]`.
Add a dedicated provider+model row editor (add/remove), sourced from the
same `getGlobalModelOptions()` the composer picker uses, that reads and
writes the `{provider, model}` chain. Half-filled rows are kept in local
state so the config autosave never persists a partial entry, and an
out-of-catalog model stays selectable so existing custom entries render.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Asserts each {provider, model} entry renders as its own row (the bug
produced "[object Object]"), that removing a row emits the remaining
entries, that adding a blank row never persists a partial pair, and the
empty-state hint.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The docs state feedback_buttons requires rich_blocks: true, but _maybe_blocks rendered full Block Kit whenever feedback_buttons alone was enabled — implicitly turning on rich-block rendering the user never opted into. Align the code with the documented contract and add a regression test.
Slack's June 30 Agent messaging experience changelog lists Bolt Python 1.29.0 / Python SDK 3.43.0 as the Agent View minimums. Bump the messaging/slack extras and the platform.slack lazy-install pins to match, and regenerate uv.lock. All adapter API surfaces verified present against the new versions in a clean venv.
…ead of dropping them MCP tool results with non-image binary resources (PDFs, archives, office docs) were silently dropped: the success path only handled TextContent and ImageContent, so a PDF-returning MCP tool appeared to return metadata only. - EmbeddedResource blob contents are decoded (50MB cap), materialized into the Hermes document cache via cache_document_from_bytes (sanitized filename, traversal-safe), and surfaced as a local-path marker the agent can read with file/terminal tools. - EmbeddedResource text contents are inlined directly. - ResourceLink blocks preserve the URI and point the agent at the server's read_resource tool; no arbitrary network fetch outside the MCP session. - AudioContent blocks are cached via cache_audio_from_bytes as MEDIA: tags. - read_resource blob contents are materialized the same way instead of returning '[binary data, N bytes]'. - Unsupported blocks are logged instead of silently discarded. - Existing ImageContent MEDIA: behavior unchanged. Reported by an enterprise customer; reproduced against an HTTP MCP server returning application/pdf resources.
…e text in isError path Follow-ups on top of NousResearch#64061's salvage: - ResourceLink markers now point at mcp__<server>__read_resource (the actual registered tool name via mcp_prefixed_tool_name) instead of a nonexistent <server>_read_resource the agent could hallucinate-call. - The isError path now surfaces EmbeddedResource .resource.text blocks instead of dropping them, so error payloads carried in resources no longer collapse to a bare 'MCP tool returned an error'. (Same-class fix flagged in NousResearch#64061 and independently addressed in NousResearch#63576 by @alauer.) - 3 new error-path tests + updated ResourceLink wire-name assertion.
…ckdrop-toggle feat(desktop): add a chat backdrop on/off toggle
…Research#64597) Every auxiliary task block (vision, web_extract, compression, title_generation, curator, background_review, moa_reference, ...) now accepts a reasoning_effort shorthand: auxiliary: compression: reasoning_effort: low vision: reasoning_effort: none _get_task_extra_body() folds it into extra_body.reasoning, which every auxiliary wire already translates: chat.completions passes it through, the Codex Responses adapter maps it to top-level reasoning/include, and the Anthropic auxiliary adapter now forwards it into build_anthropic_kwargs(reasoning_config=...) (previously hardcoded None). An explicit extra_body.reasoning on the same task wins over the shorthand. Invalid levels are ignored with a warning. Empty string (the shipped default) is a no-op — zero behavior change. Config: reasoning_effort added to all 16 auxiliary task blocks in DEFAULT_CONFIG (no version bump — deep-merge handles new keys).
…d-stale-base-url # Conflicts: # cli.py # gateway/slash_commands.py # scripts/release.py
🚨 CRITICAL Supply Chain Risk DetectedThis PR contains a pattern that has been used in real supply chain attacks. A maintainer must review the flagged code carefully before merging. 🚨 CRITICAL: Install-hook file added or modifiedThese files can execute code during package installation or interpreter startup. Files: Scanner only fires on high-signal indicators: .pth files, base64+exec/eval combos, subprocess with encoded commands, or install-hook files. Low-signal warnings were removed intentionally — if you're seeing this comment, the finding is worth inspecting. |
…onnect ladder can't freeze silently (NousResearch#66377) The Telegram gateway could go silently deaf for hours: the reconnect ladder stalled mid-way (e.g. "attempt 4/10, reconnecting in 40s" then nothing) while the process stayed active(running), so Restart=always never fired. Root class: every recovery path — the ladder's re-entry (_schedule_polling_recovery), the pending-update probe (_probe_pending_updates), and PTB's error callback — gates new recovery on _polling_error_task.done(). If that single task wedges on any hung await, all recovery returns early forever and nothing retries. The heartbeat loop is a separate task, so make it an independent, cause-agnostic watchdog: if the same recovery task stays in-flight past _POLLING_ERROR_TASK_STUCK_TIMEOUT (300s — well beyond a healthy ladder attempt's bounded stop+drain+start+backoff), force a retryable-fatal so the background reconnector rebuilds the adapter instead of relying on the frozen ladder. This guarantees progress regardless of *where* the stall is (issue direction #1), tracked locally so no task-assignment site needs to change. Also salvages @koduri-mahesh-bhushan-chowdary's NousResearch#66492 (drain-await timeout), which closes the one concrete wedge vector documented in the incident (_drain_polling_connections' unbounded shutdown()/initialize() on a wedged CLOSE-WAIT pool). The watchdog covers the rest of the class. Co-authored-by: Koduri Mahesh Bhushan Chowdary <mkoduri73@gmail.com>
…reaming Two real render-cost wins found by inspection (no behavior change): 1. Sidebar re-rendered on every stream token. $sessionStates is republished on every message delta (tens/sec during a turn), and the derived ID computeds ($workingSessionIds, $attentionSessionIds, $backgroundRunningSessionIds) allocated a fresh array each time. nanostores notifies on !==, so the whole ChatSidebar + every mounted row re-rendered per token even when the working/ attention/background set was unchanged. Return the previous array reference when the contents match → nanostores skips the notify unless the set actually changes. Turns streaming from O(visible rows)/token into O(0) for the sidebar. 2. Tool rows normalized the FULL uncapped detail every render. `looksRedundant` (lowercase + whitespace-collapse over the entire read_file/terminal payload) ran twice in the ToolEntry render body, so every completed tool re-normalized its whole output on every stream tick of the running message. Memoize on the view fields so it recomputes only when the tool's content changes. Both are correctness-preserving (stable refs + memoization). The CI stream scenario drives $messages directly, not the publishSessionState path, so it won't reflect #1 — verified by inspection.
Blocking #1 — gateway-connecting-overlay.tsx reduced-motion regression: the top `if (reduce) setPhase('gone')` fired unconditionally on mount whenever reduce-motion was on, so every OS reduced-motion user lost the CONNECTING overlay during cold boot entirely (jumped to 'gone' before the gateway was even open). The intent was to skip the exit *choreography*, not to skip showing the overlay. Removed the unconditional top block and the redundant nested preview block; kept only the third branch (`gatewayState === 'open' && shownRef.current` → `reduce ? 'gone' : 'text-out'`) which correctly gates the short-circuit on connect. Also fixed `if(reduce)` missing-space, 6-space misindent, and the same 3-line comment pasted three times. Nit #1 — tsconfig excludes e2e, so specs were never typechecked in CI. Added tsconfig.e2e.json (extends base, includes e2e/ + playwright.config.ts, adds @playwright/test types) and wired it into the typecheck script. This surfaced three latent type errors that are fixed in the same commit: - fix-electron-tracing.ts: `app._context` and `electron._playwright` are private APIs — added `as any` on the access before the existing cast. - playwright.config.ts: `reducedMotion: 'reduce'` directly under `use:` is not a valid UseOptions property in playwright 1.58; it's a BrowserContextOption accessed via `contextOptions: { reducedMotion: 'reduce' }`. The old form was silently ignored at runtime, so reduced-motion emulation wasn't actually active — screenshots could catch overlays mid-fade (exactly what the comment warned about). Nit NousResearch#2 — fix-electron-tracing.ts reaches into Playwright internals (_playwright, _allContexts, _context) with no public contract. Added a header comment calling out the `@playwright/test` exact pin (=1.58.2) so a future bump knows to re-verify the private symbols still exist. Nit NousResearch#3 — main.ts TEST_WORKER_INDEX block had stray 6-space indentation. Verified: tsc -p . && tsconfig.electron && tsconfig.e2e → 0 errors; vitest boot-failure-overlay (3/3) + boot-failure-reauth (21/21) pass; npm run build clean; playwright e2e/boot-failure.spec.ts 2/2 pass.
…native extension) unicode61 indexes a CJK run as ONE token, so 2-char Korean terms (일본, 구글, 우리, ...) can never match it and the trigram tokenizer needs >=3 chars per term — any query containing a 1-2 char CJK token falls through to a LIKE full-table scan (measured 3-6.4s CPU per query on a 6.8GB production state.db; the #1 base cost behind a 12.4s session_search average on CJK workloads). This ships a ~250-line loadable FTS5 tokenizer (no deps) that wraps unicode61: maximal CJK runs inside its tokens are re-emitted as overlapping character bigrams (Lucene CJKAnalyzer semantics), everything else passes through unchanged. FTS5 phrase semantics turn consecutive sub-tokens into exact substring matching down to 2-char terms at index speed. Build: native/fts5_cjk/build.sh -> ~/.hermes/lib/libfts5_cjk.so (override: HERMES_FTS5_CJK_SO). Salvaged from PR NousResearch#65544; the schema integration lands separately on the v23 external-content layout.
…add same-pid self-reclaim guard Hardening on top of the salvaged dead-PID lease reclamation from PR NousResearch#65775 (@the3asic): - Probe via psutil.pid_exists (hard dependency; CONTRIBUTING.md critical rule #1) with the contributor's os.kill(pid, 0) POSIX probe retained only as a scaffold-phase fallback when psutil is missing. - Same-process holders (pid == os.getpid()) are never probed and never self-reclaimed — another thread's live lease is owned by the lease refresher/release path. - Any probe doubt (exceptions, permission errors) conservatively keeps the lease until normal TTL expiry; Windows stays TTL-only. - Tests: psutil-first dead-pid reclaim (probe call pinned), os.kill fallback path, probe-doubt keeps lease, same-pid no self-reclaim, legacy holder + Windows paths assert NO probe via either API.
…ch#67140) The background write guard decided ownership from `isinstance(usage_rec, dict)`, so a local skill with NO usage record passed. That successful write called bump_patch(), which created a `created_by: null` record — and the identical write was refused from then on. "Allowed exactly once, then never" is a race with our own bookkeeping, not a policy. Reproduced on main: patch #1 succeeds, patch NousResearch#2 with the same arguments is refused. Option B from the issue. Option A (split `session_review` from `scheduled_curator` and let the session fork patch user-owned skills it consulted) would widen autonomous write permission onto skills the user owns with no user present to consent — wrong direction for a no-user-present actor. - skill_manager_tool: missing and explicit-null records now resolve IDENTICALLY, both fail closed. The refusal names the reason and points at `hermes curator adopt <name>`. - background_review: both review prompts told the reviewer to patch any skill consulted in the session and claimed pinned skills could be improved, while enforcement refused both. Prompts now list pinned, external, and user-owned skills as protected, and tell the reviewer to RECOMMEND adoption instead of attempting a write that will be refused. - skill_usage: document that `created_by` is a curator-management policy flag, not a provenance claim, and add `is_curator_managed()` so call sites read as the question they ask. Field name retained — it is on disk in every `.usage.json` and renaming would strand those records. - curator CLI: `hermes curator list-unmanaged` itemizes unmanaged skills with the reason each is unmanaged (completes the NousResearch#67139 spec). Foreground writes are untouched: a user-directed edit to a user-owned skill still works, including on pinned skills. Sibling tests: 9 failures in test_skill_manager_tool.py were fixtures that created record-less skills to exercise OTHER guards (consolidation-delete, read-before-write) and relied on ownership falling through. Fixed at the fixture, since the real curator only ever operates on managed sediment. One test asserted the old "manually authored" wording; rewritten to assert the behavior contract instead of the string. Validation: 274 targeted tests + all 7 background-review files (60 tests) pass. E2E on a temp HERMES_HOME (30 checks) covers the flip, foreground writes, adoption unblocking, pin semantics, prompt/enforcement parity, and the new verb. Each new test sabotage-verified: revert the fix, confirm it goes red. Fixes NousResearch#67140
…own (NousResearch#74136) Fix-up for the cherry-picked cooldown persistence: the PR's tests mocked the DB (SimpleNamespace(_db=MagicMock())), which cannot prove the cooldown survives a restart. Replace with the production shape — a real SessionDB on disk behind the real AsyncSessionDB facade — and add a restart regression: fail a hygiene compression on runner #1, tear it down, build a fresh GatewayRunner on the SAME database, and assert the cooldown is still honored (no compression agent instantiated). Also updates the timeout test to assert the DB-backed record_compression_failure_cooldown write instead of the removed in-memory dict. Sabotage-verified: reverting gateway/run.py to the in-memory dict makes the restart test fail.
Users following abbreviated links guess /docs/quickstart and /docs/installation and hit raw GitHub-Pages 404s — the real pages live under /docs/getting-started/. Add client redirects for both. Consumer-onboarding audit finding #1, Aug 2026.
The #1 patch failure class in production (state.db mining, 250k-window) is a re-send of an edit that already landed: 'old_string and new_string are identical' (299 occurrences) plus a share of hunk-not-found errors where the new text is already in the file. These errored, sending models into re-read/re-patch loops. New tools/fuzzy_match.is_already_applied(content, old, new) — a conservative check requiring (1) non-trivial new_string (>=8 chars), (2) EXACT presence of new_string, (3) old_string gone (unless identical). Wired into three sites: - patch_replace (replace mode): returns success + no_change: true + an explicit note instead of the identical-strings / no-match error. - V4A validation phase: an already-applied hunk validates as a no-op so multi-hunk patches no longer fail wholesale when one hunk landed in a prior call. - V4A apply phase: mirrors the same skip so the two phases agree. Genuine no-matches (new text absent) and half-applied renames (old text still present) keep their error behavior — covered by tests.
process(action='wait') hitting its window returned status='timeout' with a terse note — models read it as an error and re-issued identical waits (process is the #1 exact-duplicate tool call in production: 511 dupes in a 400k-msg window; wait is 57% of all process actions). The timeout result now carries: - process_running: true — machine-readable 'this is a status, not a failure' - an explicit note: 'Wait window of Ns elapsed — the process is still running. This is not an error. Uptime: Ms.' plus the right next step: when notify_on_complete is set, 'you will be notified on exit — do more work instead of waiting again'; otherwise a pointer to notify_on_complete for next time. - the clamp note (requested > max) now composes with the status note instead of replacing it. Exited/interrupted results are unchanged.
…e-review #1) revoke_commit_admission() used to invoke the holder-qualified lease release unconditionally — including while an admitted commit was still mutating SessionDB — letting a second compressor acquire the durable lock mid-commit and interleave with the first commit's writes. The admission_revoked flag store stays lock-free, but the lease-release decision now coordinates with the fence lock: - revoke acquires the fence lock non-blocking; on success no commit can be in flight (an admitted commit retains the lock until finish_commit) and the release runs immediately, still under the lock so a racing begin_commit cannot slip between the check and the release. - on failure the release is deferred: finish_commit() re-checks _admission_revoked and performs it AFTER the commit completes (prompt even if the worker thread is later parked), and the begin_commit refusal path does the same for a revoke that lost the race to a transient lock-setup/cancel boundary. All paths are idempotent with the worker's own outer cleanup (DB release is holder-qualified). Invariant encoded + tested: no second compressor can acquire the durable lock while an admitted commit is still mutating; after a post-revoke commit finishes the lease is released promptly. Both regressions (revoke-during-commit deferral, revoke-before-commit immediate release + refused begin_commit) are sabotage-verified.
…rst run The first-run provider picker showed Fireworks AI alongside Nous Portal before the user opened the 'Other providers' disclosure. Only Nous Portal should be visible up front; Fireworks now lives inside the expanded list but keeps its #1 position there (Nous -> Fireworks ordering preserved).
Addresses both review findings on the remote-gateway download PR: 1. Unbounded buffering (finding #1). fetchBuffer / fetchBufferViaOauthSession accumulated the entire response (then copied it again via Buffer.concat) before saveGatewayFile even opened the save dialog, so a large gateway file could exhaust the native process. Both auth paths now stream: once response headers arrive the connect timeout is cleared, the filename is derived, the save dialog is shown, and the body is piped to the chosen destination with backpressure. A read/write error tears down the stream and unlinks the partial file. The byte-moving, data-URL decoding, and filename/path helpers are extracted into gateway-file-download.ts so they're unit-testable without Electron. 2. No fallback for older gateways (finding NousResearch#2). saveGatewayFile required the new /api/fs/download route. Desktop and the remote gateway update independently, so a gateway predating this PR 404s. Added a 404-only compatibility fallback to the existing capped /api/fs/read-data-url route (bounded, so it only serves smaller files — enough to keep older backends working). Tests: gateway-file-download.test.ts covers streaming, backpressure, error-cleanup (unlink on write/response error), data-URL decoding, filename derivation (incl. traversal reduction), and 404 detection; gateway-file-download-transport.test.ts asserts both transports stream (no whole-body Buffer.concat) and that the 404 fallback is wired. Both registered in the desktop platform test list. Server-side /api/fs/download tests (streaming + sensitive-file reject) already pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…-renders (NousResearch#81726) The scoped find walker wraps transcript text nodes in <mark> elements that React does not own. Assistant responses stream through markdown-text.tsx, which rebuilds the markdown DOM on every delta, and a new message is appended whenever the assistant answers — so a re-render of a changed region detaches the marks we inserted, dropping the user's highlights while the bar stays open. Watch the captured scope with a MutationObserver and re-wrap only when an unmarked occurrence of the active query actually reappears. The observer is gated behind a re-entrancy flag while the walker is mutating, coalesced to one re-apply per microtask, torn down when the bar closes or the query clears, and restores the active ordinal so a mid-stream re-render doesn't reset the user's place to match #1. An append that adds no matching text is a no-op; re-wrapping only fires when highlights genuinely went stale. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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 NousResearch#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>
Temporary intra-fork PR to run GitHub Actions for upstream NousResearch/hermes-agent PR NousResearch#47533.\n\nUpstream PR: https://github.com/NousResearch/hermes-agent/pull/47533\n\nThis is for CI signal only; do not merge this PR into the fork unless intentionally updating fork/main.