Commit Graph

21735 Commits

Author SHA1 Message Date
Brooklyn Nicholson 1dd5c9de83 fix(update): launch acceptance before the terminal event, on both orchestrators
gille's round-2 review: the terminal lifecycle claimed outcomes the
launch hadn't delivered yet.

- posix finish() reorders: outcome -> durable result+marker -> LAUNCH
  WITH ACCEPTANCE -> terminal event. mac acceptance is open's exit code
  (launchd rejects broken bundles loudly); linux verifies the setsid
  child is still alive 1.5s after spawn, so an instant exec failure
  downgrades to a held 'manual' state + truthful result instead of a
  vanished 'done'. Gated skew/manual outcomes publish a real 'manual'
  event (new third shim state -- still zero logic in the page).
- Renderer-free linux recovery: when no chromium-family browser exists,
  manual/error outcomes fire notify-send/zenity/kdialog best-effort so a
  gated non-relaunch is never a silent disappearance.
- windows.ps1 mirrors the contract: Start-DesktopRelaunch returns
  verified acceptance (WMI pid alive / fallback process alive; dying
  before the window appears counts as failure), and the finally block
  downgrades to Show-ManualFinale + rewritten result when the launch
  didn't land. Error path still relaunches after showing itself.
- repro.sh launch / npm run update:repro:launch: real-orchestrator
  matrix for instant-exit relaunch downgrade and skew-message surfacing.
- posix.sh cds into the install root before hermes update (found by the
  sandboxed behind-repro: parts of the update resolve the mutated tree
  from cwd, which is the Desktop's cwd -- it updated the DEVELOPER'S
  checkout while reporting success against the sandbox).
2026-08-11 11:07:17 -05:00
Brooklyn Nicholson f121cd8a06 fix(update): run hermes update from the install root + unbreak fresh repro
The posix orchestrator inherited the Desktop's cwd, and parts of the
update pipeline resolve the tree they mutate from the working directory
-- the sandboxed behind-repro caught it updating the DEVELOPER'S primary
checkout (cwd at spawn time) while reporting success against the
sandbox. cd "$INSTALL_ROOT" before running hermes update, matching the
cwd:updateRoot contract of the deleted in-app path. Verified: rerun
leaves the outside checkout untouched (reflog clean).

repro.sh fresh used a --no-interactive flag install.sh doesn't have;
non-TTY stdin (</dev/null) + --skip-setup is the real non-interactive
contract.
2026-08-11 11:07:17 -05:00
Brooklyn Nicholson bdb4cfd35e fix(update): posix hand-off truth ordering, relaunch-gate port, JSON escaping
Address helix4u's review:

- finish() now delivers the outcome BEFORE publishing it: mac bundle swap
  and the linux relaunch gate run first, then the result file, marker
  removal, and the shim event -- the app launch itself goes last so it
  can't race the result write. A gated/skewed linux install (AppImage/
  deb/rpm, broken sandbox helper) surfaces its message in the result file
  AND holds the shim window open with it instead of closing on a false
  'Opening Hermes...'.
- mac swap is transactional with a checked rollback; a failed install
  restores the previous bundle and the result says so (exit 7 when even
  rollback fails). Failed 'open' rewrites the result truthfully.
- linux gate is an exact port of the deleted update-relaunch.ts logic:
  anchored path-segment match on <root>/apps/desktop/release/linux-unpacked,
  chrome-sandbox absent = namespace build = fine, present = root+setuid
  required, with the real opt-outs (ELECTRON_DISABLE_SANDBOX, --no-sandbox
  among replayed args, or the Desktop vouching) instead of the invented
  HERMES_DESKTOP_NO_SANDBOX. collectRelaunchArgs/sandboxFallbackFromEnv
  live in updater-process.ts again; the Desktop passes filtered launch
  args (after --) and --relaunch-cwd so a deep-link or --no-sandbox
  launch survives the update.
- result/status JSON strings are escaped (git permits '"' in branch
  names) and the result write is atomic (tmp + rename).
- coverage: resolvePosixScriptHandoff + ported helpers in
  updater-process.test.ts (19 pass); repro.sh gate / npm run
  update:repro:gate asserts the whole gate matrix and round-trips a
  hostile branch name through the result JSON.
2026-08-11 11:07:17 -05:00
Brooklyn Nicholson ca5b015e72 test(update): sandboxed repro paths as npm scripts
scripts/desktop-update/repro.sh drives the real code paths against a
disposable HERMES_HOME under /tmp: shim/shim-fail (UI dry runs), fresh
(literal install.sh), behind N (rewound checkout driven forward by the
orchestrator), error (broken venv -> abort + result file). Exposed as
npm run update:shim / update:shim:fail / update:repro:* from
apps/desktop.
2026-08-11 11:07:17 -05:00
Brooklyn Nicholson c9f0b6824e refactor(desktop): replace the in-app posix updater with the hand-off
applyUpdatesPosixInApp is gone: mac/linux Update now quits into the
detached posix orchestrator, same shape as Windows. Deletes everything
the in-app path dragged into main.ts -- runStreamedUpdate, the rebuild
retry, the relaunch-outcome matrix (update-relaunch.ts/update-rebuild.ts
and tests), shellQuote, resolveHermesCliBinary -- and with the app dead
before the update starts, the HERMES_DESKTOP_CHILD_PID reaper-exclusion
dance (#37532) is structurally unnecessary on the desktop path.
2026-08-11 11:07:17 -05:00
Brooklyn Nicholson c991e3f62f feat(update): posix hand-off orchestrator (mac/linux quit-first updates)
scripts/desktop-update/posix.sh is the mac/linux twin of windows.ps1:
the Desktop spawns it detached and QUITS; it waits the app out, runs
plain hermes update (retry-once across the update boundary, truthful
desktop-rebuild completion), swaps/relaunches the .app bundle (mac) or
the release/*-unpacked binary when its sandbox helper is launchable
(linux), writes .hermes-update-result.json, and drives the same shim.
Repo-owned, so every update refreshes the code that drives the next one.

resolvePosixScriptHandoff mirrors the Windows resolver (with the
flat-path fallback covering the scripts/ reorg skew).
2026-08-11 11:07:17 -05:00
Brooklyn Nicholson 503e61b3b1 feat(update): shim UI + event channel for the Windows hand-off
scripts/desktop-update.ps1 moves to scripts/desktop-update/windows.ps1 (a
compat forwarder stays at the old path for one asar/checkout skew cycle)
and gains the shim: scripts/desktop-update/ui.html rendered in a
chromeless Edge app window, fed done|error over a loopback /progress
endpoint. The page is #75895's hand-off screen ported verbatim (Fourier
Flow loader, one title, one line, OS light/dark, charcoal dark seeds);
failure is the terse card pointing at hermes debug share. The WinForms
card stays as the no-Edge fallback, same shape.

Salvaged from the web-shell spike: TcpListener runspace server, Edge
--app spawn with throwaway profile, degradation ladder, -SelfTestUi.

Co-authored-by: Teknium <127238744+teknium1@users.noreply.github.com>
2026-08-11 11:07:17 -05:00
Brooklyn Nicholson 854ab7f6df fix(desktop/windows): quiet minimal update hand-off window
The hand-off script's WinForms window was a 720x420 dashboard: streaming
log box, wide marquee, warning label. Updating is a wait, not a dashboard
-- it is now the same shape as the other update surfaces (#75895): a fixed
280x320 panel, marquee loader, one title, one static line, following the
OS light/dark theme (charcoal #232323 seeds, never brand blue).

Failure gets a terse finale instead of a wall of log: 'Failed to update' +
'Run "hermes debug share" in a terminal to send a report' + Close (held
max 5 minutes, then the relaunched Desktop re-surfaces the result banner
as before). The result-json message points at debug share too.

With nothing streamed to the window, the per-line stdout pump is gone:
Invoke-HermesStep drains both pipes async (no deadlock on chatty children,
no frozen marquee on quiet ones) and writes full output to the hand-off
log afterwards, where hermes debug share picks it up.
2026-08-11 11:07:17 -05:00
kshitij f51aa6a9b5 fix(ci): repair red main — busy-mode test + missing checkout in skills-index workflows
Three separate reds on main. Two are fixed here; the third needs no code.

1. tests/gateway/test_multiplex_busy_input_mode.py (blocks every merge)

Fails "Python tests / Run tests slice 5/12" and therefore "All required
checks pass". Semantic merge conflict between two PRs merged ~1h apart:

  a31be480 fix(gateway): respect routed profile busy modes             (added the test)
  c8f235a1 feat(gateway): allow selective multiplex profile serving    (added the gate)

c8f235a1 taught _profile_name_for_source to reject a route whose target
profile is not in the served set (profiles_to_serve). Each PR was green on
its own base; neither ran against the other's merge result.

The test asserts a route to profile "research" resolves to that profile's
busy mode, but never patches profiles_to_serve — so it reads the runner's
REAL on-disk profiles. "research" is not among them, the route is rejected
before the busy-mode snapshot is consulted, and the assertion gets the
gateway default:

  WARNING gateway.run: Rejecting profile route 'research-chat':
                       target profile 'research' is not served
  AssertionError: assert 'interrupt' == 'steer'

Patch profiles_to_serve for the assertion — the same seam every sibling
test in tests/gateway/test_profile_resolution.py already patches
(test_route_inside_allowlist_resolves, test_route_outside_allowlist_rejects).

This also removes an ambient-state dependency: the test previously passed
or failed based on which profiles happened to exist on the machine running
it. Verified passing under an empty HERMES_HOME.

Test-only. The serving gate from c8f235a1 is correct and left intact.

2. Skills-index workflows: local action used without actions/checkout

check-freshness has failed on all 12 of its last 12 scheduled runs:

  ##[error]Can't find 'action.yml', 'action.yaml' or 'Dockerfile' under
  '.../.github/actions/get-app-token'. Did you forget to run
  actions/checkout before running your local action?

./.github/actions/get-app-token is a LOCAL composite action and cannot
resolve without the repo on disk. skills-index-freshness.yml had no
checkout step at all. The step is gated on `status != 'ok'`, so the
watchdog broke exactly when it was supposed to file its issue — the live
index is currently 521.4h stale (limit 26h) and nobody was told.

An audit of all workflows for this bug class found one more instance:
skills-index.yml's `trigger-deploy` job, which re-triggers the docs deploy
so a refreshed index reaches the live site. Its sibling `build-index` job
checks out; this one did not. That is plausibly why the index went stale
in the first place. Both are fixed; the audit now reports zero remaining
jobs that use a local action without a prior checkout.

Pinned to the same actions/checkout SHA used by the other 35 call sites.

3. "Publish inline E2E evidence" — no fix needed

Failed once at 13:33Z on a transient TLS error reaching api.github.com
("certificate is not valid for any names") while installing a gh
extension. The last 25 runs of that workflow are 25/25 success. Infra
blip, not a code defect.
2026-08-11 21:26:28 +05:30
kshitij 936dd7346f chore: add landaun to contributor email map for #83906 salvage 2026-08-11 20:37:06 +05:30
kshitij 48c233d503 fix(gateway): offload remaining atomic_json_write calls in async paths
Completes the bug class from #83906 — the same blocking fsync-on-event-loop
pattern existed in two more async gateway paths:

- slash_commands.py _handle_restart_command: two atomic_json_write calls
  for .restart_notify.json and .restart_last_processed.json were blocking
  on fsync inside an async function. Now offloaded via asyncio.to_thread.

- run.py _clear_restart_failure_count: called from
  _handle_message_with_agent (async, per-turn path) after a successful
  agent turn. Made the method async and offloaded the atomic_json_write
  call via asyncio.to_thread. Caller updated to await.

Shutdown-path calls in _stop_impl_body (_increment_restart_failure_counts,
planned restart notification marker) are intentionally left synchronous —
the event loop is draining/stopping and offloading adds complexity for no
benefit.
2026-08-11 20:37:06 +05:30
landaun 5bf47cfced test(gateway): assert the channel-directory write leaves the event loop
Mirrors test_discord_builder_runs_off_event_loop_thread. Verified to FAIL
against unpatched v0.19.0 and pass with the fix.
2026-08-11 20:37:06 +05:30
landaun 0cf48bca25 fix(gateway): run channel-directory write off the event loop
atomic_json_write() calls os.fsync(), which blocks until the write
reaches stable storage. build_channel_directory() already offloads its
builders with asyncio.to_thread (#60794) but still called the persist
step directly on the loop, so the Discord heartbeat waited on a disk
flush.
2026-08-11 20:37:06 +05:30
kshitij c0106e50e7 fix(kimi): send Hermes attribution headers instead of claude-code/0.1.0
The Kimi team noticed that traffic from Hermes Coding Plan users
identifies itself as Claude (User-Agent: claude-code/0.1.0) rather
than the actual client. They asked us to update the UA so they can
properly attribute traffic and understand how their services are
accessed — especially important as they open up to more third-party
agents.

Three code paths were sending wrong/attribution-less headers to Kimi:

1. run_agent.py — _apply_client_headers_for_base_url sent
   {"User-Agent": "claude-code/0.1.0"} for api.kimi.com. Now sends
   the same _AI_GATEWAY_HEADERS set used for Vercel AI Gateway:
   HTTP-Referer + X-Title + HermesAgent/{version} User-Agent.

2. agent/anthropic_adapter.py — the Anthropic Messages path for
   api.kimi.com/coding sent 'claude-code/0.1.0'. Now sends the same
   three-header attribution set.

3. plugins/model-providers/kimi-coding/__init__.py — both kimi and
   kimi_cn profiles sent a static 'hermes-agent/1.0' with no
   HTTP-Referer or X-Title. Now sends the full three-header set with
   a dynamic version, matching the pattern used by the gmi, fireworks,
   xai, and ai-gateway provider profiles.

The attribution header set (HTTP-Referer + X-Title + User-Agent) is
the canonical Hermes pattern used for OpenRouter, Vercel AI Gateway,
Fireworks, and other providers that read these headers for traffic
attribution.
2026-08-11 13:25:37 +05:30
ethernet 3ee1bb323e fix(ci): resolve fork PRs in the E2E evidence publisher
The publisher read the PR number from the CI run's pull_requests
payload. GitHub keeps that payload empty for fork runs, so the job
printed 'No pull request is associated' and stopped on every fork PR.

Resolve the PR from the run's head owner, branch, and SHA instead.
The SHA match skips runs that a newer push superseded.

A fork PR also has no CI review comment, because the live poller
skips forks. The publisher now logs this and exits clean instead of
raising; the evidence stays in the workflow artifact.
2026-08-11 03:53:52 -04:00
Trevin Chow 9829746dfe fix(gateway): require explicit route rejection marker 2026-08-10 22:48:24 -07:00
Trevin Chow c8f235a106 feat(gateway): allow selective multiplex profile serving 2026-08-10 22:48:24 -07:00
Trevin Chow a31be48030 fix(gateway): respect routed profile busy modes 2026-08-10 21:51:46 -07:00
Teknium 9d6c5a920c fix(desktop): collapse Fireworks behind the provider disclosure on first 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).
2026-08-10 21:09:27 -07:00
brooklyn! 697f2896bf
Merge pull request #83648 from NousResearch/bb/custom-provider-profile
fix(desktop): scope custom provider settings to the active profile
2026-08-10 22:47:38 -05:00
Brooklyn Nicholson 1db3405c0d test: assert profile scoping against on-disk config and .env
get_env_value/load_config read through the shared os.environ mirror that
save_env_value writes, so a reader-based assertion cannot prove which
profile's store actually received the write. Read the two profiles'
config.yaml and .env directly instead, and cover the credential path.
2026-08-10 22:37:16 -05:00
Brooklyn Nicholson 1e6a7b3315 fix(desktop): scope custom provider settings to the active profile
The custom-endpoint REST handlers ran bare load_config/save_config, so
every add/activate/delete landed in the process-level default profile
regardless of which profile the desktop settings UI was targeting. A
provider added under a non-default profile silently went to default:
visible only in default-bound sessions, absent everywhere else, and
un-addable to another profile without hand-editing its config.yaml.

Scope all four handlers (list/upsert/activate/delete) to the requested
profile via _config_profile_scope, matching /api/config, and spread the
active profile into the four hermes.ts wrappers alongside their existing
validateCustomEndpoint sibling.
2026-08-10 22:26:54 -05:00
Teknium 2cdb30a474 chore: contributor email mapping for salvaged commit 2026-08-10 19:16:59 -07:00
Math 1edfdeee81 fix(desktop): keep serve backend alive through Windows launcher 2026-08-10 19:16:59 -07:00
hermes-seaeye[bot] 6a20383eeb
fmt(js): `npm run fix` on merge (#83609)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-11 02:09:04 +00:00
Teknium 0a60b164f5 fix(desktop): single-owner console capture + HUD lifecycle coverage
Reconcile the salvaged #81533 lifecycle helper with the renderer-log
console pipeline that landed in #83535 (the two PRs raced):

- window-renderer-lifecycle.ts no longer handles console-message —
  renderer-log.ts is the single owner (per-window labels, boundary
  reports). One owner means no double-logged errors on windows wearing
  both, and OAuth/portal windows (lifecycle-wired for process events)
  cannot spill third-party page console output into desktop.log.
- wake indicator window gets attachRendererConsoleCapture, keeping the
  console coverage it previously got from the helper.
- HUD window (added after the PR branched) gets log-only lifecycle
  coverage — it was the one renderer window the PR couldn't have known
  about.
- Tests updated: lifecycle helper asserts it attaches NO console-message
  listener; parser tests live in renderer-log.test.ts.
2026-08-10 19:00:19 -07:00
Chen Jin 0c1a11ada6 fix(desktop): correct import order for window-renderer-lifecycle before window-reveal (#81290)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-10 19:00:19 -07:00
Chen Jin d966393814 fix(desktop): wire renderer-lifecycle diagnostics into OAuth and portal login windows (#81290 follow-up)
@spfcraze's triage review noted the PR description claimed "every
BrowserWindow" but the OAuth and portal sign-in windows were not wired:
a crashed sign-in renderer leaves the window's promise path never
settling, with no trace in desktop.log.

Wire both with the same log-only lifecycle diagnostics as the overlay
and quick windows — `kind: 'oauth'` and `kind: 'portal'` respectively.
Neither window gets crash-reload treatment (a sign-in window that
reloads itself mid-auth would be surprising); the lifecycle helper's
log-only callback is the exact contract needed here.

window-renderer-lifecycle.test.ts: 17/17 pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-10 19:00:19 -07:00
Chen Jin fae0309c00 chore: drop accidental .omc/ gitignore change from PR 2026-08-10 19:00:19 -07:00
Chen Jin 11f920b812 fix(desktop): attach renderer-lifecycle diagnostics to all BrowserWindow instances (#81290) 2026-08-10 19:00:19 -07:00
Teknium 33f8e96a72 fix: guard has_env profile probe with _safe like its sibling fields
An unreadable profile dir made (entry / '.env').exists() raise
PermissionError out of the sidebar fallback, 500ing /api/profiles.
Found by hostile fixture during live E2E of the scandir conversion.
2026-08-10 18:04:59 -07:00
Teknium 0d21eb82b5 chore: contributor email mapping for salvaged commit 2026-08-10 18:04:59 -07:00
Teknium e2e0f1677c test: isolate _ACTION_PROCS in #52470 spawn test so lifespan shutdown hooks don't trip on its poll-less fake 2026-08-10 18:04:59 -07:00
Michael Gannotti 373631bea1 fix(dashboard): raise fd soft limit + replace iterdir with scandir to stop fd leak (#81547)
Two-part fix for the dashboard fd exhaustion reported in #81547:

1. Raise RLIMIT_NOFILE soft limit on startup (before uvicorn binds).
   macOS defaults to 256 for LaunchAgent processes — too tight for the
   dashboard which opens 3 fds (db+wal+shm) per SessionDB per request
   across all profiles. After days of polling the soft limit exhausts
   and every os.listdir/open raises OSError [Errno 24]. The helper raises
   to the hard limit (or minimum 4096), matching the reporter's ulimit
   workaround. No-op on Windows (no resource module).

2. Replace bare Path.iterdir() with context-managed os.scandir() in four
   dashboard hot paths: _fallback_profile_dicts, file manager list,
   checkpoint listing, and plugin discovery. iterdir() returns a
   generator that holds an open directory fd until fully consumed; if
   an exception interrupts iteration the fd leaks. os.scandir() is an
   explicit context manager that guarantees close on exit, following
   the same idiom already used in /api/fs/list.

Tests: 6 passed, 3 skipped (resource-module tests skip on Windows).
2026-08-10 18:04:59 -07:00
a-espinoza 91de3beb72 fix(desktop-ssh): raise remote backend file limit 2026-08-10 18:04:59 -07:00
Quark Assistant 0b15eb5f05 fix(desktop): terminate app-managed gateway on shutdown 2026-08-10 18:04:59 -07:00
Quark Assistant eb9fc9ad7f test(desktop): reproduce orphaned gateway on serve shutdown 2026-08-10 18:04:59 -07:00
RelaxJonh 07298df805 fix(gateway): reap orphaned gateways before spawning restart (#77276)
_spawn_gateway_restart() now calls _reap_unsupervised_gateway_orphans()
before spawning a new `hermes gateway restart` child.  On desktop-app
restart the old serve exits but its gateway child gets reparented to
launchd (PPID=1) and keeps its platform connection alive.  The new
serve then spawns a fresh gateway, resulting in two live gateways
racing the same connection.

The reap was already implemented for the CLI restart path (#75936) but
the dashboard's _spawn_gateway_restart path was not covered.

Fixes #77276
2026-08-10 18:04:59 -07:00
hermes-seaeye[bot] a1bfbccc02
fmt(js): `npm run fix` on merge (#83539)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-11 00:20:02 +00:00
Teknium e5e2fb8b2d fix(desktop): persist renderer crashes to desktop.log + finish the render() isolation class
Three-part class closure for the React #310 / lost-renderer-crash family
(#79428, follow-up to #80560 / #82763):

1. Diagnosability (#79428 defect B): error-boundary catches now persist to
   desktop.log with their component stack via a new fire-and-forget
   hermes:logs:renderer-error IPC (synchronous flush — the window may be
   dying). Every renderer-content window (main, secondary session, instance,
   HUD, quick entry, pet overlay) gets the error-level console capture that
   previously only the main window had, labeled per window. 'Open logs' on
   the crash dialog now reveals a file that actually contains the crash.

2. Recurrence guard: eslint no-restricted-syntax rule banning inline
   render() calls in JSX — the mechanism behind #80560. The rule
   immediately caught two live sites #82763's audit missed (floating
   panes, narrow-overlay reveal), both hosting plugin panes.

3. Fix those two missed sites with the same ContribRender mount.

extracted console-capture/report formatting to electron/renderer-log.ts
with unit tests; renderer console lines now carry the window label.
2026-08-10 17:10:53 -07:00
Teknium 9b1a2a14ca fix: use psutil.pid_exists for orphan-reap liveness probe (Windows footgun lint)
os.kill(pid, 0) sends CTRL_C_EVENT on Windows (bpo-14484). The reap path
is POSIX-only, but the blocking lint rejects the pattern repo-wide and
psutil is a core dependency.
2026-08-10 17:02:56 -07:00
Teknium 1485a4ac2b chore: contributor email mappings for salvaged commits 2026-08-10 17:02:56 -07:00
cadezhou bc1223840d fix(desktop): reap orphan gateways at startup
On Desktop serve startup, reap orphan gateway processes (PPID=1) left
behind by a previous serve session that exited abnormally. This prevents
the old and new gateways from racing for the same QQ WebSocket
credential, which splits messages across parallel session trees (#77276).
2026-08-10 17:02:56 -07:00
Leon Phull 888624ae61 fix(cli): never reap serve processes owned by a valid backend.lock.json
Production incident: the orphan reap killed a legitimate SSH remote backend
started by another client machine. Its process sat at ppid 1 with the same
cmdline shape as a genuine orphan, and the exclusion list only covered THIS
app instance's children — ownership by OTHER clients was invisible.

The reap now treats every backend.lock.json under ~/.hermes/desktop-ssh/*/
as an ownership claim: lock payloads are schema-validated (mirroring
remote-lifecycle.ts) and their PIDs are excluded both before the scan and
re-checked after it (defense in depth against a lock written mid-scan).

Regression tests cover the exact incident shape: a lock-owned PID and a
genuine orphan with identical process shapes — only the orphan is reaped.

Also: fold the new single-field `runtime` config category into `agent`
(_CATEGORY_MERGE) and fix an env leak in the serve-startup test
(HERMES_SERVE_HEADLESS restored via monkeypatch) so the combined suites
run green in any order.
2026-08-10 17:02:56 -07:00
Leon Phull 585cee1a42 fix(gateway): persist RLIMIT_NOFILE floor into the generated launchd plist
launchd starts children with soft nofile=256; hermes gateway start rewrites the plist and previously stripped any manually-added SoftResourceLimits, silently reintroducing EMFILE crashes under load. The plist generator now embeds the configured runtime.nofile_soft_limit so the persisted service definition and the in-process floor share one knob.
2026-08-10 17:02:56 -07:00
Leon Phull b06f79a100 test(runtime): use anyio mark to match repo test framework 2026-08-10 17:02:56 -07:00
Leon Phull 6386c75306 fix(desktop): reap orphaned local serve backends on desktop boot
When Desktop exits uncleanly, leftover `hermes serve --host 127.0.0.1 --port 0`
processes can be reparented to pid 1 and keep full MCP trees alive. The next
boot then stacks another backend on top of the corpses until EMFILE kills
sidebar/session APIs and tabs disappear.

- Detect Desktop-local serve shape (loopback + ephemeral port 0)
- Only reap processes whose ppid is 0/1 (true orphans)
- Spare fixed-port remote serves (e.g. --port 9119) and HERMES_DESKTOP_CHILD_PID
- Run at Desktop backend start (HERMES_DESKTOP=1) before parent-death watchdog

Complements parent-death watchdog (prevents future orphans) and configurable
nofile soft limit (capacity floor). Together these stop the multi-backend
pile-up cascade observed on macOS Desktop SSH/local installs.
2026-08-10 17:02:56 -07:00
XiaoZAZA a9a0648f49 fix(desktop): reap orphaned serve backends via parent-death watchdog + group-kill
An unclean desktop exit (crash / SIGKILL / update handoff) stranded every
`hermes serve` profile backend as an orphan (ppid=1) still serving, each
holding its MCP child subtree — 31 orphans / ~1.3 GiB RSS on one install.

Root causes + fixes:
- serve had no parent-death watchdog: add _start_parent_death_watchdog() in
  web_server.py (mirrors slash_worker.py), gated on HERMES_PARENT_PID; os._exit
  cascades to MCP watchdogs. No-op for standalone `hermes serve`.
- desktop passes HERMES_PARENT_PID in both serve spawn env blocks (main.ts).
- POSIX teardown now group-kills (process.kill(-pid, ...)) so MCP grandchildren
  die too (backend-child.ts + waitForBackendExit SIGKILL fallback).

Windows path unchanged (forceKillProcessTree). Tests updated + passing.
2026-08-10 17:02:56 -07:00
Eva d93913b421 test(runtime): cover nofile edge cases 2026-08-10 17:02:56 -07:00
Eva acb7547dac fix(runtime): make nofile soft limit configurable 2026-08-10 17:02:56 -07:00