Commit graph

287 commits

Author SHA1 Message Date
Brooklyn Nicholson
254aeda122 fix(desktop): dedicated reader for large remote attachments
Keep Settings-configurable preview/image loads on readFileDataUrl, and
route remote non-image attach through a 256 MiB IPC so uploads are not
stuck on the 16 MiB default after #73221.

Co-authored-by: Börje <borje@dqsverige.se>
2026-07-28 19:20:14 -05:00
Brooklyn Nicholson
c14ae3796e feat(desktop): fs-watch the plugin dir + demote the status snapshot
- watchDirectory IPC (same registry/channel as the preview file watchers)
  replaces the disk-plugin door's 5s readdir poll; older shells without
  the capability keep the poll, which self-upgrades to the watch once the
  plugins dir exists.
- Status snapshot: 15s → 60s, skips round-trips while hidden, and
  refreshes immediately on visibilitychange so re-focus never shows stale
  health.

Part of #73618.
2026-07-28 17:21:58 -05:00
hermes-seaeye[bot]
66e20786d0
fmt(js): npm run fix on merge (#73666)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-28 21:55:38 +00:00
Teknium
03af12c5c9 style(desktop): blank line between node: and vitest import groups
perfectionist/sort-imports requires spacing between builtin and
external import groups.
2026-07-28 14:47:36 -07:00
Teknium
20e0b26c4e test(desktop): run venv-blocker-scan tests under vitest
CI's electron vitest project collects electron/**/*.test.ts; the
node:test-based suite reported 'No test suite found'. Import
describe/it from vitest like sibling electron tests.
2026-07-28 14:47:36 -07:00
Teknium
1dd1c449bb fix(desktop): run venv-blocker scan async off the main-process event loop
The preflight's execFileSync froze the Electron UI event loop for up to
15s while psutil scanned the full process table. Convert
scanVenvBlockers to async execFile and await it in applyUpdates; tests
updated to async DI stubs. Also adds trailing newlines.
2026-07-28 14:47:36 -07:00
atakan g
d210500b54 fix(desktop): surface external venv update blockers 2026-07-28 14:47:36 -07:00
brooklyn!
c8cdeb435f
fix(desktop): allow renderer camera capture (#73558)
* fix(desktop): allow camera capture through the permission handlers

The session permission hooks were written for the voice composer and denied
video outright, so any renderer getUserMedia({video}) failed with
NotAllowedError before the OS was ever consulted.

Rename isAudioCapturePermission to isMediaCapturePermission and accept video
alongside audio in both the request and check handlers. The OS capture
permission still applies, so the user keeps a real allow/deny.

* build(desktop): declare camera usage for signed macOS builds

A hardened-runtime build needs the camera entitlement and an
NSCameraUsageDescription string, or the packaged app is killed on first
camera access instead of prompting.
2026-07-28 14:50:52 -05:00
atakan g
d4221b2736 fix(desktop): validate WebSocket token before backend ready 2026-07-28 12:35:57 -07:00
Brooklyn Nicholson
eaf30a4de7 build(desktop): declare camera usage for signed macOS builds
A hardened-runtime build needs the camera entitlement and an
NSCameraUsageDescription string, or the packaged app is killed on first
camera access instead of prompting.
2026-07-28 13:40:44 -05:00
Brooklyn Nicholson
47cb4ea1fe fix(desktop): allow camera capture through the permission handlers
The session permission hooks were written for the voice composer and denied
video outright, so any renderer getUserMedia({video}) failed with
NotAllowedError before the OS was ever consulted.

Rename isAudioCapturePermission to isMediaCapturePermission and accept video
alongside audio in both the request and check handlers. The OS capture
permission still applies, so the user keeps a real allow/deny.
2026-07-28 13:40:44 -05:00
hermes-seaeye[bot]
2faac36866
fmt(js): npm run fix on merge (#73552)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-28 18:30:19 +00:00
Adolanium
b5dc471152
feat(desktop): make attachment data-URL size limit configurable (#73221)
Hard 16 MB cap on readFileDataUrl blocked larger local attaches with no way to raise it. Settings -> Chat now has a free-form MB field. Main process owns the persisted value and clamps only absurd inputs.
2026-07-28 09:10:21 -04:00
brooklyn!
7ddf3f1500
Merge pull request #73161 from NousResearch/bb/default-zoom-out
feat(desktop): default UI zoom to Appearance 90% preset
2026-07-28 04:08:15 -05:00
Brooklyn Nicholson
4a9754b3c3 feat(desktop): default UI zoom to Appearance 90% preset
Land on the exact 90% scale so the settings control shows a selection
on fresh installs, instead of five Ctrl+- steps (~91%) between presets.
2026-07-28 04:00:55 -05:00
Brooklyn Nicholson
5b32f9dc36 feat(desktop): default UI zoom to five steps out (~91%)
Ship Ctrl/Cmd+- ×5 from Chromium 0 as DEFAULT_ZOOM_LEVEL so fresh
installs open tighter. Actual Size / Ctrl+0 and mandatory garbage
fallbacks all land on the same level; existing zoom-state.json is
left alone.
2026-07-28 01:35:31 -05:00
Brooklyn Nicholson
070093a318 feat(desktop): on by default for dev-server runs
Gating this behind an opt-in was the wrong call. A dev server already
executes arbitrary local JS — vite's module graph, every postinstall in
node_modules — so a loopback debugging port does not meaningfully widen
what a `npm run dev` session can already do, and `perf:serve` has opened
one unconditionally all along.

Requiring the variable also defeated the point: the tooling exists to be
reached for mid-task, and a capability you must remember to enable before
launching is one you don't have when you need it.

So the port opens on 9222 — the same port scripts/eval.mjs and
scripts/perf/lib/cdp.mjs already default to — for any dev-server run.
HERMES_DESKTOP_CDP_PORT stops being an on-switch and becomes an
override: a different port, or `off` to disable.

The hard gate is unchanged and still checked first: a packaged build
never opens the port, and no env value talks it into it. Neither does an
unpackaged `electron .` against dist/, which is how the packaged app
gets smoke tested.

Refusals only log when they contradict something the developer asked for
(a typo'd port, an explicit `off`). Packaged and dist runs are closed by
design and stay quiet.
2026-07-27 23:52:33 -05:00
Brooklyn Nicholson
6254c568c8 feat(desktop): opt-in renderer debugging port for dev runs
The renderer is a Chromium page, and apps/desktop already carries a whole
CDP toolkit for it — scripts/eval.mjs, scripts/perf/lib/cdp.mjs with its
shared SELECTORS map, and the diag-*/probe-* family. None of it can
attach to `hgui` or `npm run dev`, because neither passes
--remote-debugging-port. The only launcher that opens one is
`npm run perf:serve`, which is a separate isolated instance rather than
the app you're looking at.

Add HERMES_DESKTOP_CDP_PORT. When set, the shell opens a CDP port on
loopback so that existing tooling can read the live DOM: computed
styles, geometry, which rule actually won.

Three independent gates, all required, resolved by a pure function in
electron/dev-cdp.ts so the policy is testable without an Electron app:

  1. not packaged — a shipped build never opens the port, and this is
     checked first so no env combination can talk it into doing so;
  2. HERMES_DESKTOP_DEV_SERVER present — an unpackaged `electron .`
     against dist/ is how the packaged app gets smoke tested, so it
     behaves like the packaged app here;
  3. the port explicitly requested and a valid integer.

Default `npm run dev` is unchanged and silent: no port, no nag. An
opt-in that gets refused always logs why, so nobody loses an hour
wondering what isn't listening.

The address is pinned to 127.0.0.1 rather than left to Chromium's
default, and is deliberately not configurable — there's no reason to
expose a renderer debugger off-host and offering the knob invites
someone to try.

scripts/eval.mjs hardcoded :9222 and threw a raw ECONNREFUSED stack when
nothing was there. It now honours the same variable and explains itself.
2026-07-27 23:23:47 -05:00
Brooklyn Nicholson
579b66336f fix(desktop): let automated teardown quit past the active-work prompt
Playwright closes the app with a turn still in flight, so the new quit
confirmation waited on a click nobody was there to make and the E2E
worker died on a 90s teardown timeout.
2026-07-27 16:19:27 -05:00
Brooklyn Nicholson
9ae3bd73c9 feat(desktop): confirm before quitting with a turn in flight
Cmd-Q went straight through to teardown, killing the backend mid-tool-call
— the turn is gone and whatever the agent was part-way through writing
stays part-way written, with nothing on screen to warn about it.

Renderers now report which chats are mid-turn; before-quit merges the
reports and asks, naming them, defaulting to Keep Running. Update, swap,
and uninstall relaunches skip the prompt: those are the app replacing
itself, and a modal there would strand the detached script waiting on a
PID that never exits.
2026-07-27 15:47:57 -05:00
Brooklyn Nicholson
97a8034dfd refactor(desktop): resolve profile backend routing from one table
Three helpers each re-derived part of the same decision: which backend
serves profile P, and does its REST path need a `?profile=` scope.
profileUsesPrimaryBackend answered the first half, pathWithGlobalRemoteProfile
answered the second, and ensureBackend re-checked globalRemoteActive() around
both. Splitting one table across three predicates is how the global-remote
case ended up registering reapable pool entries for a backend it never owned.

resolveProfileBackendRoute() states the four routes in one place and returns
the backend, the descriptor scope, and whether the path needs a query
parameter. The call sites read the answer instead of recomputing it.

One behavior change falls out: `hermes:api` now passes the primary profile
through, so the primary no longer sends itself a redundant `?profile=<self>`
on a global remote that already serves it.
2026-07-27 13:44:11 -05:00
Brooklyn Nicholson
f18e50a070 fix(desktop): record main-process faults in desktop.log
Electron pre-installs its own uncaughtException listener and only warns on
unhandled rejections, so a main-process fault usually leaves the app running
with the reason on stderr — which nothing captures when the app is launched
from Finder or the Start menu. The fault never reaches desktop.log, so it is
absent from `hermes debug share` and the user can only describe symptoms.

Record both to desktop.log and flush synchronously, since a fault that does
prove fatal leaves no chance for the batched async flush. Five loadURL calls
were also unhandled, each able to leave a blank window with no explanation
anywhere the user can send us; they now name the surface that failed.

Co-authored-by: Rodrigo Fernandez <rod@nxtlevel.dev>
2026-07-27 13:41:59 -05:00
Brooklyn Nicholson
d7e738af90 fix(desktop): retire pooled remote backends whose host went away
A pooled backend entry pointing at a remote host has no child process, so
the 'exit' handler that clears a dead local backend never fires. The
renderer's 60s keepalive touch also spares it from the idle reaper. Nothing
was left to retire the descriptor, so once the host went away the pool kept
serving it and every profile bound to that host stayed broken until restart.

Pooled remote descriptors now share the primary's liveness policy: probed on
the same revalidate tick, keyed per base URL, and dropped only after the
same consecutive-failure limit, so the next ensureBackend() rebuilds.

Co-authored-by: Rodrigo Fernandez <rod@nxtlevel.dev>
2026-07-27 13:41:59 -05:00
Gille
3884e0eea0 fix(desktop): reuse global remote backend across profiles
Keep non-primary profiles that inherit the app-global remote on the primary connection descriptor instead of creating processless pool entries that the idle reaper repeatedly removes.

Preserve per-profile remote overrides and local pooled backends, and cover the routing policy with behavioral tests.

Co-authored-by: Rodrigo Fernandez <rodrigo@nxtlevelsaas.com>
2026-07-27 13:41:59 -05:00
Vitor Cepeda Lopes
704a321870 fix(desktop): preserve OAuth sessions in sidebar 2026-07-27 13:41:56 -05:00
hermes-seaeye[bot]
8c36cd4670
fmt(js): npm run fix on merge (#72411)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-27 02:52:53 +00:00
Andy
651a313d2b fix(desktop): suspend decorative work when inactive 2026-07-26 21:05:55 -05:00
Ho Lim
7a5d534f5b fix(desktop): gate idle renderer loops
Signed-off-by: Ho Lim <166576253+HOYALIM@users.noreply.github.com>
2026-07-26 21:05:55 -05:00
teknium1
d6fdae6746 feat(desktop): quick-entry window — global hotkey capture to any session
Parity with Claude Desktop quick-entry-window / ChatGPT Quick Chat.
2026-07-26 17:48:19 -07:00
teknium1
aa3121bb34 feat(desktop): Quick Entry — global-hotkey mini composer window 2026-07-26 17:48:19 -07:00
David Metcalfe
31385a0102 test(desktop): cover find-in-page helpers and multi-window routing
Vitest coverage for apps/desktop/electron/find-in-page.ts.
The helpers and the IPC handlers in main.ts are the only
consumers, so the tests pin the wire shape (match counter
shape, options defaults, no-throw-on-destroyed) and the
multi-window correctness that the original CJS PR missed:

- formatFoundInPage:
  - Maps { activeMatchOrdinal, matches } → wire payload.
  - Coerces missing fields to zero (the renderer never
    sees NaN).
  - Tolerates null / undefined inputs.

- performFind:
  - Forwards query + options to webContents.findInPage.
  - Defaults forward=true and findNext=false when omitted.
  - Treats null / non-object options as "all defaults".
  - Coerces a non-string query to string (defensive against
    a misbehaving renderer).
  - Is a no-op on null webContents.
  - Is a no-op on destroyed webContents (does not throw
    across the IPC boundary).

- stopFind:
  - Calls stopFindInPage with the default action
    ('clearSelection').
  - Honors an explicit action argument.
  - Is a no-op on null or destroyed webContents.

- installFoundInPageForwarder:
  - Forwards 'found-in-page' to the sender as a formatted
    payload.
  - Handles missing fields without throwing.
  - Skips send when webContents is destroyed at fire time.
  - Returned uninstall removes the listener.
  - Returned uninstall on null/destroyed webContents is a
    safe no-op.
  - Regression: two forwarders installed on distinct
    webContents do not cross-fire. This is the bug the
    original PR shipped — the global mainWindow listener
    routed results to the primary regardless of which
    renderer invoked findInPage. Pinning this here keeps
    the per-sender routing from regressing.

Run with:
  cd apps/desktop && npx vitest run electron/find-in-page.test.ts --project electron
2026-07-26 17:48:05 -07:00
David Metcalfe
7bbb063c71 feat(desktop): port find-in-page bridge to TypeScript Electron
The original PR targeted the CJS Electron files
(apps/desktop/electron/main.cjs and preload.cjs), but commit
39d09453f "feat(desktop): ts-ify everything" renamed them to
main.ts and preload.ts on current main. The PR's diff therefore
targeted files that no longer exist on main.

Brings the bridge forward to the current TypeScript Electron
files and extracts the IPC bridge helpers into a focused
pure-helpers module:

- apps/desktop/electron/find-in-page.ts (new):
  - performFind(webContents, query, options) — wraps
    webContents.findInPage with default-coercing options.
  - stopFind(webContents, action) — clears highlights.
  - formatFoundInPage(result) — pure projection of
    Electron's FoundInPageResult onto the wire payload shape
    ({ activeMatchOrdinal, count }).
  - installFoundInPageForwarder(webContents) — wires a
    sender-scoped 'found-in-page' forwarder; returns an
    uninstall function. Returns a no-op uninstall for null
    or destroyed webContents so callers don't need guards.

- apps/desktop/electron/main.ts:
  - ipcMain.handle('hermes:find-in-page', event => ...)
    resolves the requesting window via
    BrowserWindow.fromWebContents(event.sender) and routes
    the search to THAT window, not the global primary. This
    fixes a multi-window bug where Cmd+F pressed in a
    secondary session window (one per chat, spawned via
    hermes🪟openSession) searched the primary window
    instead of the focused surface.
  - ipcMain.handle('hermes:stop-find-in-page', event => ...)
    routes stopFind through the requesting window for
    multi-window correctness.
  - A per-sender lazy forwarder registry
    (foundInPageForwarders: Map<webContentsId, () => void>)
    installs installFoundInPageForwarder on first
    findInPage call, scoped to the sender's webContents.
    Cleans up automatically via webContents.once('destroyed',
    ...). The forwarder sends results back to the SAME
    renderer that initiated the search, never the global
    primary — so a secondary session window's Cmd+F shows
    matches from THAT window and the match counter reports
    matches from THAT window's DOM.

- apps/desktop/electron/preload.ts:
  - hermesDesktop.findInPage(query, options) — invokes
    the IPC handler.
  - hermesDesktop.stopFindInPage() — invokes the IPC
    handler.
  - hermesDesktop.onFoundInPage(callback) — subscribes to
    'hermes:found-in-page' results from the sender;
    returns an unsubscribe function so the FindBar can
    clean up on unmount.

- apps/desktop/src/global.d.ts:
  - Three new hermesDesktop method declarations:
    findInPage, stopFindInPage, onFoundInPage. The new
    forwarder install registers a 'found-in-page' listener
    bound to the sender's webContents and emits
    'hermes:found-in-page' results back to the sender.

The multi-window fix is part of the same port — the old
PR's behavior (Cmd+F in a secondary session window searched
the global primary) was a bug present in the CJS files,
not a design constraint we wanted to preserve. The new
helper module uses event.sender by design, so the
multi-window correctness lands with the TS port.

Fixes #46169
2026-07-26 17:48:05 -07:00
Brooklyn Nicholson
53707e405c fix(desktop): stop repair from deleting a healthy install's marker
Repair signalled "reinstall me" by deleting the bootstrap marker. That was
already destructive -- repair is reachable from a transient backend error on a
fully working install -- and it stranded users in first-run setup with no way
back short of hand-writing the marker file.

Carry the intent in an explicit flag instead. Repair forces the next resolve
through the installer and clears itself once the reinstall starts, so a forced
reinstall still works without destroying provenance about how the install was
created.

Closes #72166
2026-07-26 16:18:01 -05:00
Brooklyn Nicholson
048e9b2150 fix(desktop): launch a usable active runtime without a bootstrap marker
Marker presence was the launch gate, but the marker is provenance about who
ran the install -- not proof the runtime works. A CLI-installed repo+venv, or
a healthy install whose marker a repair deleted, both read as "never
installed" and dropped the user into first-run bootstrap on every launch.

Split the two questions: classifyActiveRuntime() reports marker validity and
runtime usability separately, and the resolver launches whenever the runtime
is usable, logging when it proceeds without a marker. An unusable runtime
still falls through to bootstrap even with a valid marker, so an interrupted
install can't spawn a dead backend.

Drops isBootstrapComplete(), which had no callers left once the gate moved.

Co-authored-by: iveywest <iveywest@users.noreply.github.com>
Co-authored-by: lihengming <lihengming@users.noreply.github.com>
2026-07-26 16:12:14 -05:00
hermes-seaeye[bot]
0ce9022e03
fmt(js): npm run fix on merge (#71740)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-26 04:11:26 +00:00
Brooklyn Nicholson
75456183b6 fix(desktop): wire the credentialed readiness probe and reauth latch into boot
Connects the preceding seams to the boot path:

- buildReadinessHealthProbe picks the probe credentials from the connection's
  authMode via resolveReadinessProbeAuth, and reports whether the probe is
  credentialed so waitForHermesReady can read a 401 correctly. The bearer
  goes through fetchJson's `bearer` option — a raw `headers` object is
  ignored there, which would have silently probed uncredentialed.
- authMode is threaded into all three waitForHermes call sites: the primary
  remote connect, the profile pool backend, and the SSH tunnel (loopback
  forward, token auth), so an old backend behind a tunnel gets the same
  compatibility path.
- The confirmed reauth failure latches in remoteReauthFailure and
  short-circuits startHermes, and is cleared on every recovery path —
  resetHermesConnection (soft/hard apply and reconnect), bootstrap reset,
  repair, and a CONFIRMED oauth sign-in. A cancelled or closed login window
  leaves the latch set so the overlay stays actionable.
- gatewayAuthProviders reads the public /api/auth/providers (cached per base
  URL, failures return []) so the oauth pre-flight guard can skip its hard
  failure for an all-password gateway while keeping the strict guard
  everywhere else.
2026-07-25 21:52:51 -05:00
Brooklyn Nicholson
36e2228a4c fix(desktop): latch a confirmed remote reauth failure so the overlay stays clickable
shouldLatchBackendStartFailure deliberately never latches a remote failure:
remote faults are usually transient and must stay retryable without an app
restart. A confirmed reauth rejection is the exception — it cannot self-heal,
because nothing changes until the user signs in again.

Worse, not latching actively prevents the recovery it was protecting. A
non-latching remote boot failure re-runs startHermes on every
getConnection/api call, re-emits running: true, and the boot-failure overlay
(visible = Boolean(boot.error) && !boot.running) hides itself — so the
"Sign in" button flickers out from under the user before it can be clicked.

Add shouldLatchRemoteReauthFailure as a separate predicate rather than
changing the existing one, so transient remote failures keep self-healing.
The two latches are complementary and never fire for the same failure.
2026-07-25 21:52:45 -05:00
Brooklyn Nicholson
cf4fb8993f fix(desktop): name the readiness-probe auth and password-gateway guard decisions
Two pure seams in native-auth-decisions.ts, following that module's existing
pattern — the value is the test that pins the contract so the god-file call
sites can't drift back to the buggy shape.

resolveReadinessProbeAuth decides how the boot readiness probe
authenticates, delegating to resolveOauthRestAuth for the oauth case rather
than duplicating the bearer-vs-cookie rule.

oauthGuardMayHardFail fixes a second way a gated gateway is misread.
authModeFromStatus maps the gateway's `auth_required: true` onto 'oauth',
but that flag only means the dashboard is GATED — it says nothing about how
you authenticate. A gateway whose providers are all username/password can
satisfy neither of the pre-flight guard's checks by construction:
start_login raises NotImplementedError, /auth/native/authorize rejects
password providers, and its cookies come from a plain password-login POST
rather than the /auth/callback redirect the OAuth partition is primed for.
The guard therefore threw "uses OAuth, but you are not signed in" one line
before a mintGatewayWsTicket that would have succeeded against that very
partition.

The helper returns false only when EVERY advertised provider is
password-based; an unknown or empty list keeps the strict guard, so backends
predating /api/auth/providers are unaffected.
2026-07-25 21:52:40 -05:00
Brooklyn Nicholson
8d025489cf fix(desktop): read a readiness-probe 401 by whether it was credentialed
The boot readiness probe calls the credential-free /api/health, and only a
404 flips it to the /api/status fallback. Both halves are wrong against a
gated gateway.

/api/health landed in ccab46ca4 (2026-07-24), after the v2026.7.20 tag, so
every container pinned to a release lacks the route. On those backends the
dashboard auth gate runs ahead of the SPA catch-all, so an unknown /api/*
path is rejected as unauthenticated rather than 404 — a credential-free
probe can never observe the 404 that the fallback keys on, and boot loops
until the 45s deadline reporting a healthy backend as "did not become
ready". Upgrading the backend is not a workaround for release-pinned
deployments.

Simulating a 0.19.0 backend (both the route and its PUBLIC_API_PATHS entry
removed, since ccab46ca4 added the two together) shows the probe's 401 means
two different things depending on whether credentials were sent:

  credential-free: /api/health -> 401 no_cookie, /api/status   -> 200
  credentialed:    /api/health -> 404,           /api/sessions -> 200

So the fix is not "fall back on any 401". Uncredentialed, a gate-shaped 401
identifies a missing route and must fall back. Credentialed, a 401/403 is a
rejected session and must fail fast — falling back would hit the public
/api/status, get a 200, and report a dead session as ready, deferring the
no_cookie to the first real API call.

Split the two cases and tag the credentialed rejection as a terminal reauth
error. A generic 401 without the gate shape, plus 429 and 5xx, keep polling
as before.
2026-07-25 21:52:26 -05:00
Perseus Computing
ac327dfa38
fix(desktop): resolve unpacked spawn-helper before chmod (#71171) 2026-07-25 15:19:29 -04:00
izumi0uu
23cb26c2e3 fix(desktop): probe /api/health for boot readiness, and survive a stalled loop
Desktop boot polls /api/status, so readiness waits on gateway config and a
cold plugin import tree. On Windows that regularly outlives the probe and
Desktop kills a backend that is already listening, respawns it, and re-pays
the same import cost — the reported crash loop.

Probe /api/health instead, falling back to /api/status only for the
missing-route shapes the fetch helpers emit (404, or HTML from the SPA), so
an older remote backend still connects. Timeouts and server errors keep
polling health rather than dropping to the heavyweight route.

A cheap route is not enough on its own. Warming the gateway import holds the
GIL, so the event loop can stall for tens of seconds and starve /api/health
too. At the default 15s socket timeout only three attempts fit in the 45s
budget; give each probe 5s so the loop keeps retrying across the stall.

Co-authored-by: webtecnica <75556242+webtecnica@users.noreply.github.com>
Co-authored-by: DESXIE <78300229+DESXIE@users.noreply.github.com>
Co-authored-by: frohsinnllc <231045016+frohsinnllc@users.noreply.github.com>
2026-07-24 19:44:48 -05:00
hermes-seaeye[bot]
bb4765d21c
fmt(js): npm run fix on merge (#71099)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-25 00:35:03 +00:00
webtecnica
d68e043ba7 fix(desktop,update): prevent silent state.db zeroing during Windows update (#68474)
Problem:
On Windows, state.db could be silently replaced with 95MB of null bytes
during a desktop update (v0.19.0). The pre-update snapshot was valid, but
the live file was destroyed and the update reported exit code 0, masking
the data loss. Sessions between the snapshot and the update were
irrecoverable.

Root cause analysis:
The update flow (Desktop Electron → hermes-setup.exe → hermes update)
kills the backend process tree via taskkill /T /F, then pauses Windows
gateways, creates a pre-update snapshot, runs git pull + pip install, and
resumes gateways. On Windows, a force-killed process holding state.db
(SQLite WAL mode) can leave the file open to races with antivirus/NTFS
filter drivers, or the gateway resume can encounter a partially-recovered
WAL state that results in a zeroed file — all while exit code 0 reports
success.

Fix — three layers of defense:

1. Emergency desktop-side backup (pre-flight):
   - New  function in Electron main.ts reads the
     SQLite header, logs it, and takes a timestamped emergency copy of
     state.db BEFORE the backend is killed or the updater is spawned.
     Runs in both the Tauri-updater path (Windows) and the in-app update
     path (Posix). Prunes to the 2 most recent emergency backups.

2. Pre-update integrity verification (Python CLI):
   - After  creates the pre-update snapshot,
      checks the LIVE state.db file (header +
     PRAGMA integrity_check). If corrupted, checks whether the snapshot
     copy is valid and warns the user. The update still proceeds because
     the snapshot is the recovery path.

3. Post-update auto-restore (Python CLI):
   - After the update completes (both git-pull and ZIP paths), verify
     state.db integrity. If corrupted/zeroed, automatically restore from
     the pre-update snapshot and re-verify. This catches the exact case
     where state.db was destroyed mid-update but the snapshot was valid.

New functions in hermes_cli/backup.py:
  - verify_sqlite_integrity(path, check_header, run_pragma, max_bytes)
    → Three-stage check: file size, SQLite header magic, PRAGMA
      integrity_check. Configurable max_bytes to avoid reading huge DBs.
  - copy_db_and_verify(src, dst)
    → Like _safe_copy_db() but verifies the destination after backup.

Fixes #68474
2026-07-24 15:59:32 -07:00
teknium1
0fb0ba475d fix(windows): platform._syscmd_ver stub in bootstrap + PYTHONUTF8 in desktop backend env
Two gaps found auditing the decode-crash cluster:

1. suppress_platform_ver_console() only ran in hermes_cli.main processes;
   slash workers, tui_gateway/entry, run_agent, batch_runner, and cli.py
   import only hermes_bootstrap and were exposed to both the console
   flash and (on Python 3.11.0/3.11.1, which lack CPython's
   encoding='locale' fix) a UnicodeDecodeError inside platform.win32_ver()
   under PEP 540 — the crash #69413 reported. Move the stub into
   hermes_bootstrap so every entry point gets it; the _subprocess_compat
   copy stays for non-bootstrap callers.

2. The desktop Electron spawn built the backend env without PYTHONUTF8,
   so anything the Python child emitted before hermes_bootstrap ran
   (interpreter startup errors, pre-bootstrap tracebacks) decoded with
   the locale default. Re-port of PR #56499's env half (echoriver89) to
   backend-env.ts (original targeted the deleted backend-env.cjs);
   explicit user setting wins.
2026-07-24 15:47:12 -07:00
cat-that's-fat
7dd2b2dc77 feat(desktop): add "Connect to existing Hermes" option to first-run onboarding
Adds a first-run Desktop choice between installing Hermes locally and
connecting to an existing remote Hermes gateway. The choice appears after
backend resolution but before ensureRuntime(), so selecting remote cannot
accidentally trigger local bootstrap.

New modules:
- first-run-setup-gate: concurrent first-run decision gate and reset semantics
- primary-backend-startup: Electron-free orchestration seam (saved remote
  resolution, gate decision, remote re-resolution, local continuation)
- primary-connection-rehome: prevents dual-owner race where both cold boot()
  and renderer softSwitch() could connect simultaneously
- first-run-remote-form: extracted remote form with stale-result guards

Reuses existing connection-config IPC, encrypted token storage, OAuth
session partition, and primary backend resolution.

Fixes #38602
Fixes #36970
2026-07-24 12:55:06 -05:00
Teknium
fbf04ae079 style(desktop): apply eslint/prettier conventions to find-git-bash module
npx eslint --fix + prettier --write on the new files: braces on
single-line if returns and blank-line padding per the desktop lint
config.
2026-07-23 17:59:58 -07:00
Teknium
6c2c866a9a fix(desktop): join Windows bash candidates with path.win32 in find-git-bash
The extracted findGitBash builds Windows-style candidate paths, but the
vitest suite (and any POSIX CI host) runs with posix path.join, which
mangles 'C:\Program Files' + segments into slash-joined paths and broke
the invalid-override fallback test. Use path.win32.join explicitly so
candidate construction is host-independent.
2026-07-23 17:59:58 -07:00
seamusmore
6b278eeccc fix(desktop): respect HERMES_GIT_BASH_PATH in findGitBash()
Port the HERMES_GIT_BASH_PATH env var check from main.cjs to main.ts
after the TS conversion. Also extract findGitBash to a dedicated module
for testability and add focused regression tests for override precedence
and invalid-override fallback.
2026-07-23 17:59:58 -07:00
hermes-seaeye[bot]
7d96e602a9
fmt(js): npm run fix on merge (#69805)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-23 03:32:32 +00:00
ethernet
3651627d88
fix(desktop): skip bootstrap for explicit Nix backend (#69688)
Treat HERMES_DESKTOP_HERMES as an authoritative deployment override.
This keeps the Nix desktop package on its matching immutable Hermes CLI
instead of falling through to install.sh when a best-effort version probe
fails or times out.
2026-07-22 20:00:48 -04:00