Commit Graph

44686 Commits

Author SHA1 Message Date
Austin Pickett
79dbb1450e fix(tools): make managed Node authoritative in the MCP stdio PATH
`_prepend_path` inserted the resolved command's directory only when it was
absent from the child's PATH. The Hermes installer appends its managed Node
dir to the user PATH, so for anyone with a system Node (<22.12) earlier on
PATH the check no-oped and the managed dir stayed behind it. npm lifecycle
children (`node install.js`) then resolved the older system Node and failed
with ERR_REQUIRE_ESM even though Hermes had provisioned a compatible runtime.

Strip every existing case/trailing-separator variant of the directory first,
then prepend it, so the canonical entry is the one that wins and PATH does
not grow duplicates.

Fixes #82309
2026-09-27 01:05:37 -04:00
Austin Pickett
652fb2eb57 fix(desktop): reseed a bare provider pick the default migrated to custom:*
A manual composer pick is sticky by design (d595e636c8), so a persisted
`nvidia` pick kept riding session.create after config.yaml moved to
`custom:nvidia`. The gateway honors the body's provider over config, built the
NATIVE provider, and silently dropped the custom entry's `extra_body`
(`thinking: {type: adaptive}`), disabling adaptive reasoning.

The catalog aliases `custom:<key>` with the bare config key (#87035): the two
spellings name the SAME endpoint, so a bare pick is a stale spelling, not a
distinct choice. refreshCurrentModel() now reseeds it from the profile default
when that default is the entry's `custom:<key>` form. A pick that already names
a provider class stays sticky, and an unrelated manual pick is never clobbered.

Fixes #81922
2026-09-27 01:04:41 -04:00
Austin Pickett
f454288d78 fix(desktop): latch a resume that paints nothing when the list row is stale
The resume-failure latch ("this window is stranded, retry it") armed only
when the cached sessions-list row reported message_count > 0. That row is a
cache of backend truth and lags the two flows that report a vanished thread:
after a sleep/wake reconnect the list can still carry the respawned
backend's session at 0 rows, and a context-compression tip can show 0 rows
while the stored transcript is intact. Either way the cold resume painted an
empty thread with an ACTIVE runtime and no latch at all — no auto-retry, no
error surface, just a silently blank chat that looks like lost history.

The resume RPC is authoritative and always reports the stored transcript
size (tui_gateway fills message_count from state.db even when
messages_omitted), so treat it — and a non-empty REST page — as the other
rungs of the same ladder. A resume that paints nothing now arms the retry
latch whenever ANY source says the session has a transcript, so the window
recovers through use-route-resume's bounded retry (and surfaces an explicit
error + manual Retry once exhausted) instead of going silently blank.

Fixes #82806
Fixes #83154
2026-09-27 01:04:36 -04:00
Brooklyn Nicholson
28e6496a5e fix(web): scrub a session's on-disk artifacts in the delete endpoint
DELETE /api/sessions/{id} removed the DB row but never passed the
profile's sessions dir to SessionDB.delete_session, so the on-disk
transcript artifacts survived the UI delete: legacy session_<id>.json
snapshots (which can carry plaintext secrets) and the gateway's
request_dump_<id>_*.json dumps. The CLI delete path threaded the
directory all along; the endpoint was the outlier.

Also sweep the legacy session_<id>.json snapshot name in
SessionDB._remove_session_files so deletes and prunes clear it from
installs whose older builds wrote it.

Fixes #60207

Co-authored-by: izumi0uu <izumi0uu@gmail.com>
2026-09-26 22:52:51 -05:00
Austin Pickett
181af69ffe test(desktop): gate the Windows ancestor-spare tests on the Windows lane
Mark the win32 rows with platforms("windows") instead of patching
sys.platform: the repo runs host-specific behaviour on that host, and the
Windows Python-tests lane already imports this file.
2026-09-26 23:50:01 -04:00
John Paul Soliva
c9ccdb967d fix(desktop): the skip notice names the in-app update, and hermes desktop reopens the app it kept
Review follow-ups on the Windows skip:

- Most people who hit the boot loop launched Desktop from the Start menu and
  never open a terminal. The in-app update also rebuilds the app: the
  Windows shim waits for Desktop to exit, and the already-up-to-date path
  still completes with desktop=True. The notice and updating.md now name
  Update now in Settings -> About next to `hermes desktop`.
- cmd_gui took the skip's None as "no launchable app was found". A build
  that fails raises instead of returning None, so None there only means the
  skip, and `hermes desktop` now reopens the app that was kept.

Tests fold to the two invariants, each red when its half of the fix is
reverted: the stop spares its ancestor Desktop on every platform, and a
Windows packaged build under its own Desktop is skipped (now driven through
the real _desktop_ancestor_in with a fake process tree, instead of a stub).
2026-09-26 23:50:01 -04:00
John Paul Soliva
8f338a845a docs(update): the Windows Desktop rebuild is skipped when the update runs inside the app 2026-09-26 23:50:01 -04:00
John Paul Soliva
5188378e25 fix(desktop): the Windows update tail never stops the Desktop it runs under
An update interrupted after its dependency sync leaves source-completion-
pending, and the next Desktop launch finishes it from the Desktop's own
backend (venv_sync._finish_source_update). On Windows the tail's desktop
build reached _stop_desktop_processes_locking_build, which spared ancestor
Desktops only on POSIX, so it terminated the Desktop it was running under.
The pipes every child logs through went with it, the whole tree died before
the tail could clear its markers, and every launch repeated it (#123499).

Stopping an ancestor can never free the exe lock for the process doing the
rename: that process dies first. The ancestor spare now applies on every
platform, and build_prepared_desktop skips a Windows packaged build running
under its own Desktop with a notice, since its promotion could not succeed
while that Desktop holds the lock. The tail finishes and clears its markers;
the app's content stamp stays stale, so `hermes desktop` run outside the
app rebuilds it.

Closes #123499.
2026-09-26 23:50:01 -04:00
Austin Pickett
30592c1c68 test(update): prove probe-era receipts reuse under the manifest npm version
The receipt key hashes the npm version. Cover the transition from the
child-probe lane to the manifest lane: an install completed under the
probed version must still be recognized as complete once the version is
read from npm's package.json (same string for standard npm), and that
reuse must hold when `--version` can no longer spawn — the #123933 lane
on a Windows Job Object — without running npm ci again. A changed npm
version still invalidates the receipt. Runs without symlinks so Windows
CI exercises it too.

Co-authored-by: JoaoMarcos44 <joaomarcosdias444@gmail.com>
2026-09-26 23:48:52 -04:00
liuhao1024
646c3c8ad5 fix(update): resolve npm's manifest through symlinks and fall back to the probe
npm_execpath can point through a symlink, and the manifest sits beside
the resolved CLI, never beside the link: resolve the realpath before
looking for package.json. Layouts without a readable manifest now fall
back to the pre-fix child probe instead of aborting with ENOENT.

Move the regression test to tests-js/node-deps.test.mjs (the module's
own suite) and cover the symlinked-execpath and fallback lanes there.
2026-09-26 23:48:52 -04:00
liuhao1024
9880fbb109 fix(update): read npm's version from its manifest instead of a child probe
The npm version probe spawned node-under-node before the reuse
short-circuit, so on Windows a Job-Object EBUSY spawn failure aborted
the whole dependency preparation even when the install was already
complete — leaving the pending-completion marker behind and turning
every launch into the same doomed completion pass (#123933).

npm's own package manifest states its version without any process
creation, so the probe lane can no longer fail: the install-receipt
key keeps its exact value, engine checks still run, and completed
installs reuse with zero spawns.
2026-09-26 23:48:52 -04:00
hermes-seaeye[bot]
1ff7e08d6d fmt(js): npm run fix on merge (#124746)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-09-27 03:48:45 +00:00
Brooklyn Nicholson
42d70d29ac fix(gateway): broadcast projects.changed when projects.db moves
projects.db is written by the CLI (hermes projects create), other
Desktop windows, and workspace-settings flows — processes that never
touch this gateway's transports, exactly like state.db. The change
watcher had no projects.db signature, so those writes left the Desktop's
project list and folder tree stale until an unrelated refresh (#53046,

Add a projects.changed watch (2s interval, newest mtime across
projects.db + WAL for the watcher home and every served sibling profile,
mirroring the sessions.changed contract), register the event in the
gateway contract (generated TS/OpenRPC refreshed), and route it through
the desktop lifecycle handler into a $projectsChangeTick that
refreshes both the projects list and the sidebar tree.

Fixes #53046
Fixes #56757

Co-authored-by: LeonSGP43 <LeonSGP43@users.noreply.github.com>
2026-09-26 22:37:53 -05:00
Brooklyn Nicholson
54fb5a42e8 fix(gateway): drop routing entries and transcript files on hard delete
DELETE /api/sessions/<id> removed only the state.db rows: the durable
channel->session routing index (gateway_routing table + sessions.json
mirror) survived, so the next Discord/Telegram message routed to the SAME
id and resurrected the deleted row, and the on-disk .json/.jsonl
transcripts plus request_dump files were never scrubbed because
sessions_dir was not passed to delete_session (#42422).

- SessionStore.remove_by_session_id: drop every entry pointing at the id
  (one channel can hold several) and persist the drop to both durable
  copies; the index is written back by the gateway process, so removing
  only DB rows elsewhere is undone by the next whole-index save.
- The API delete handler now passes the request-scoped sessions_dir and
  clears the routing entries through the runner's SessionStore.
- Deletes made out of the gateway process self-heal at routing time via the
  stale-route guard once a missing row counts as ended.

Fixes https://github.com/NousResearch/hermes-agent/issues/42422
2026-09-26 22:30:35 -05:00
Brooklyn Nicholson
981ec5d150 fix(gateway): treat a hard-deleted session row as a dead route
_is_session_ended_in_db answered False when the row was missing from a
readable owning DB, so a session hard-deleted out of the gateway process
(CLI/TUI/desktop local delete) kept its live channel->session route: the
next inbound message resolved the deleted id and run_agent's INSERT OR
IGNORE re-created the row with its old content — the deleted conversation
resurrected (#42422). A missing row is now the same verdict as an ended
one: the stale-route self-heal drops the entry, recovery finds nothing,
and the peer mints a fresh session. DB errors still answer False.
2026-09-26 22:30:35 -05:00
hermes-seaeye[bot]
a9109b07f4 fmt(js): npm run fix on merge (#124736)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-09-27 03:21:39 +00:00
Brooklyn Nicholson
8cb4fdc925 fix(process): heartbeats wake the agent only on new output, and never as a user bubble
A `terminal(background=true, heartbeat=N)` tick queued a notification every N seconds
whether or not the process had printed anything, and every queued event costs the owning
session a full model turn. On Desktop and the TUI that turn painted the wake as a user
bubble ("[Background process ... heartbeat #9 ... (no new output since the last
heartbeat)]") followed by the model's "Still running normally." — over and over, for a
process whose row on the status stack already said it was running — and while the wake
held the session's turn, the user's own prompt sat queued behind it.

- `ProcessRegistry._emit_heartbeat` skips a tick with no new output. The sequence counts
  delivered beats only; the "(no new output)" placeholder in the formatter is gone.
- TUI/Desktop type heartbeat rows `display_kind: hidden` (the kind both clients and the
  transcript preview already honour); the CLI paints a one-line receipt and persists the
  row hidden, so reopening the session in Desktop shows only the agent's reply.
- Desktop hydration drops heartbeat rows persisted by older backends the same way.
- `display.background_process_notifications: off` is honored by the TUI/Desktop poller and
  the CLI drain, not just the messaging gateway. `off` mutes process-driven wakes only:
  a finished `delegate_task(background=true)` still lands.

Supersedes #123123 (cherry-picked; scoped so `off` keeps subagent results) and #119202
(cherry-picked; `heartbeat: 0` is schema-valid so models that materialize every field
stop tripping the foreground guard).
2026-09-26 22:19:53 -05:00
kur4i
4317ed0e71 fix(terminal): make disabled heartbeat schema-valid
(cherry picked from commit b746557577832c1ac1fac5e54088623d1b239f2f)
2026-09-26 22:19:53 -05:00
liuhao1024
ede966b347 fix(cli): honor display.background_process_notifications=off in the process drain
The documented `off` mode gated only the gateway's completion injection
(#9290); the CLI drain never consulted the key, so setting it silently did
nothing on the CLI — leaving no escape hatch against the post-Ctrl+C
notification turn restarts (#123114).

Mirror the gateway semantics: the drain still claims and acknowledges
completion events (durable rows converge instead of replaying on restart),
but the turn-starting `_pending_input` injection is suppressed.

Fixes #123114

(cherry picked from commit d009992c9e64b6083951b27a8261d138ae669ccb)
2026-09-26 22:19:53 -05:00
Brooklyn Nicholson
9fa36ea9e2 fix(desktop): resync the Bot model picker's manual-entry state when the catalog resolves
The picker latched its manual/free-text form from useState(!isKnown) at
first paint, before the async model.options read had resolved — so a bot
whose saved provider was known to the gateway still got free-text inputs
on the first Edit open, and the dropdown appeared only on the second
open, from the cached catalog. The latch is now the user's explicit
choice only (null = no choice yet); the form derives from the LIVE
catalog state on every render.

Fixes https://github.com/NousResearch/hermes-agent/issues/121875
2026-09-26 22:11:49 -05:00
Konstantin Khlopkov
424d4bdbd4 fix(desktop): collapse duplicate @-mention rows behind a source's handle claims
The composer's @ popover listed every named profile twice: the Bot Mode
roster tags a bot by the slug of its friendly title (@john for a profile
dir named john-2), while the gateway's own @ completions list the same
backend profile by raw name (@john-2). use-at-completions deduplicated
by display text, so both labels for one routable identity survived and
the merged list truncated arbitrarily at slice(0, 8).

A composer.atCompletions row can now declare `handles` — other @texts
that resolve to the same target. The merge drops a gateway row whose
handle is claimed by a contributed row, keeping the contributed label
(title slug, connection meta). Bot Mode claims the raw profile name for
its local rows only; a remote row never claims a name the local gateway
might list as a different, local bot.
2026-09-26 22:05:24 -05:00
Brooklyn Nicholson
4d620c977b fix(desktop): one-time model snapshot adoption no longer strands catalog defaults behind a stale allowlist
A visible-models allowlist persisted before the known-models snapshot
existed was adopted with the entire current catalog marked as already
judged, so a model present in the catalog but absent from the old
allowlist stayed hidden through every later refresh. Adoption now
records only the curated defaults the old allowlist actually contained;
the ones it omitted stay unknown and the default rule re-admits them.
Explicit hide-all sentinels are honoured, and a deliberately hidden
default resurfaces once at worst — the next save records the re-hide.

Fixes the adoption half of
https://github.com/NousResearch/hermes-agent/issues/122053 — stores
already seeded by the previous adoption behavior still need the
recovery action proposed in
https://github.com/NousResearch/hermes-agent/pull/122055.
2026-09-26 22:02:46 -05:00
Brooklyn Nicholson
b384cdde05 test(desktop): match the leaf name for the -flash sub-trigger row (#51833 fallout)
Rebase fallout from main's picker/input cluster: the pointer-takeover test
matched the composed 'Gemini 2.5 Flash' text, but the -flash variant now
renders as its own chip, so the row's name span reads 'Gemini 2.5'. Target
the name span and walk up to the sub trigger, like the adapted badge tests.
2026-09-26 21:57:37 -05:00
Brooklyn Nicholson
a2587b8c9f fix(desktop): honor every documented renderer_accessibility off value (#122080 review)
YAML parses a bare `0` as int, which hit neither the bool nor the str
branch of _desktop_launch_options, so the unquoted off-switch silently
kept the tree on. Accept numeric scalars explicitly (bool checked first —
it is an int subclass), and add `disabled`/`enabled` to all three word
lists (launcher _A11Y_OFF_WORDS, readDesktopLaunchConfig, RENDERER_
ACCESSIBILITY_OFF_WORDS) so both launch paths accept the same
vocabulary.
2026-09-26 21:57:37 -05:00
Brooklyn Nicholson
adcbe3379b test(desktop): adapt #51833 badge tests to the flash variant chip
Rebase fallout: main's badge-chip tests assumed the effort chip directly
follows the name span, and matched the composed 'Gemini 2.5 Flash' text.
The -flash variant chip now sits between name and effort badge, and the
name no longer contains the variant. Find the truncating name span by
class instead of previousElementSibling, assert the name stays free of
variant and effort, and match the leaf 'Gemini 2.5' text.
2026-09-26 21:57:37 -05:00
Brooklyn Nicholson
ea1de7b98e test(desktop): mock the settle-grace keep set at module scope (#122080 review)
vi.spyOn was handed a freshly-constructed object literal, so the #118156
hook test never put 'just-archived' in the keep set — it passed with the
production fix deleted (vacuous). Hoist a module-scope mock of
getRecentlySettledSessionIds instead (same pattern as the session-removal
mock above it): four-cell control run, the test now fails on unfixed code
and passes on fixed code.
2026-09-26 21:57:37 -05:00
Brooklyn Nicholson
01db908bf7 fix(desktop): honor desktop.renderer_accessibility on packaged launches
The opt-out was bridged only by the hermes desktop launcher; a packaged app
started from its own entry never saw it. main.ts already reads config.yaml's
desktop block before ready (readDesktopLaunchConfig), so parse the key there
and set HERMES_DESKTOP_RENDERER_ACCESSIBILITY=0 when it is false.
2026-09-26 21:57:37 -05:00
Brooklyn Nicholson
ea647bd518 fix(desktop): expose the renderer accessibility tree to dictation/IME tools (#118271)
Chromium only builds the renderer's accessibility tree when it detects
an assistive technology — and dictation tools that insert text through
the OS accessibility APIs (Wispr Flow etc.) do not register the way a
screen reader does, so the tree never appeared: the window exposed only
its chrome and the contenteditable composer was unreachable. No
setAccessibilitySupportEnabled call existed anywhere in the electron
tree, on any platform.

- apps/desktop/electron/renderer-accessibility.ts (new): the
  platform/env decision in a dependency-free module (app object
  injected) — ON by default on darwin/win32 where the typed API exists,
  opt-out via HERMES_DESKTOP_RENDERER_ACCESSIBILITY=0. Linux is untouched
  (ATK flow needs no manual enable; the API is not typed there).
- main.ts calls it right after app ready (the API's requirement).
- hermes desktop launcher: `desktop.renderer_accessibility: false` in
  config.yaml bridges to the env var — only the opt-out is bridged, so
  the default never depends on the launcher path. Same shape as
  desktop.disable_gpu. config_defaults documents the key.

Family note: the same seam on Windows is #92607 (Wispr Flow there);
this one enablement should resolve both platforms, but the Windows leg
is unverified on this machine, so #92607 stays open. The dictation
keybind half of #118271 already landed separately in 3cc24b0130
(composer.dictate action; rebindable-key ask tracked by #112118/#71658,
left alone).

Tests: 4 electron-side unit tests (default-on per platform, Linux skip,
opt-out words + explicit env precedence, injected app switch flips
exactly when enabled); Python side: 12-case normalization parametrize +
env-bridging precedence test (ON sets nothing, opt-out bridges, explicit
env wins) in test_gui_command.py; _desktop_launch_options signature
consumers updated. All green; the remaining lucide-react TS2307 in
onboarding setup.tsx is pre-existing on base (missing dep of that
workspace file, untouched here).

Fixes #118271
2026-09-26 21:57:37 -05:00
Brooklyn Nicholson
26481c69ed fix(desktop): pin SearchField's clear button to the row edge (#119204)
The row is an inline-flex container, but the input was only ever
content-sized (`field-sizing: content`, no flex grow), so inside a
stretched row — every w-full settings page, the flex-1 connector filter
bars — the input kept text width and the clear button trailed the typed
text at whatever offset the value happened to be, instead of sitting at
the row's right edge.

`flex-1` joins the input's classes. `field-sizing: content` stays: it
governs the row's *preferred* size (a width-less caller like the sessions
sidebar keeps its compact, value-width row) while `flex-1` + `min-w-0`
govern what happens when the caller stretches the row — the input
absorbs the free space and the clear button (plus any trailing action)
pins to the edge. Long queries still scroll inside the field.

Layout verified in a real browser across the four caller shapes:
settings w-full (clear delta 351px -> 2px from the right edge), command
centre max-w-[40vw], kanban flex-wrap header, and the width-less
sidebar row — compact sizing unchanged in the width-less cases.

RED→GREEN on origin/main: the flex-child test fails at base (no flex-1)
and passes after; clear wiring and empty/loading cases covered alongside.

Fixes #119204
2026-09-26 21:57:37 -05:00
Brooklyn Nicholson
b0bd70e4b7 fix(desktop): keep archived sessions out of the sidebar merge (#118156)
Archiving a session removed it optimistically, but it came back minutes
later with the backend never un-archiving it (state.db still archived=1).
Two renderer paths let the row back in:

1. mergeSessionPage's survivor filter honored `hidden` and the keep set,
   never the tombstones — and the keep set reliably names a just-archived
   chat, because the 30s settle grace keeps it "recently settled" and the
   optimistic drop only clears $sessions, not a `previous` slice a slower
   refresh (messaging "Load more") captured earlier. Once ANY path put
   the row back into $sessions, the survivor filter kept it there across
   every later refresh.

2. The tombstone guard on incoming rows expires: applyProjectTreePayload
   prunes a tombstone as soon as the mutation RPC lands and the tree
   payload omits the id — correct for the tree overlay, but it also
   removes the last guard a stale recents page has to pass, so a page
   read before the archive commit could re-inject the row after the
   prune.

The survivor filter now consults $removedSessionIds directly (bare id +
lineage root, the same match dropTombstoned applies to incoming rows),
re-reading the set at merge time so a removal landing mid-refresh is
still honored. A failed RPC untombstones immediately, so nothing is
filtered on non-destructive paths.

RED→GREEN on origin/main: mergeSessionPage keeps a keep-listed tombstoned
row at base and drops it after; the hook-level race test (row seeded in
$sessions + settle-grace keep + tombstone) passes only with the filter.

Fixes #118156
2026-09-26 21:57:37 -05:00
Brooklyn Nicholson
212d1d6fba fix(desktop): split look-alike model picker rows with a -flash variant tag (#118083)
models.dev carries both `deepseek-flash` (the alias) and
`deepseek-v4.1-flash` (the full id) for the provider, so the picker lists
two rows for two distinct ids. prettifyBase() mapped them onto the SAME
label: the alias hit the `deepseek-flash` hardcode ("DeepSeek V4.1 Flash",
added in #117646) while the full id fell through to naive title-case
("Deepseek V4.1 Flash"). Neither id matched VARIANT_TAGS, so both rows
rendered tagless and read as one model listed twice with inconsistent
casing.

- `-flash` joins `-fast`/`-thinking`/`-preview`/`-latest` as a variant
  tag, so the alias renders "DeepSeek + Flash" and the full id
  "DeepSeek V4.1 + Flash" — the same disambiguation `…-4.8` vs
  `…-4.8-fast` already uses.
- The alias hardcode is gone; it was the collision's source.
- A vendor-word pass fixes the casing gap that produced "Deepseek"
  ("GLM", "MiniMax", "MiMo", "ERNIE", "OpenAI" included), so generated
  labels match the vendor's own spelling.

RED→GREEN on origin/main: the new model-status-label cases fail at base
(both ids → { name: 'DeepSeek V4.1 Flash', tag: '' }) and pass after.
Sibling menu tests updated to the corrected labels — the catalog row
locates by name with Flash as meta, and the search-fold case asserts an
id-style query still finds the row through the id fold.

Fixes #118083
2026-09-26 21:57:37 -05:00
Brooklyn Nicholson
a1d2a5bd57 fix(gateway): warn when the legacy Windows login item survives migration
_install_startup_entry swallows the .cmd unlink failure, so a locked legacy
launcher left both entries firing at logon while reconcile reported a
migration and doctor --fix counted it fixed (#80569).
2026-09-26 21:44:50 -05:00
Brooklyn Nicholson
3ddf06867f fix(update,doctor): reconcile duplicate Windows gateway autostart entries
hermes update now runs the autostart reconcile when it refreshes the
launcher scripts, and hermes doctor reports a Startup entry that
duplicates the Scheduled Task (removing it under --fix), so installs
made before the install-time fix converge too.

Co-authored-by: David Metcalfe <80915+DavidMetcalfe@users.noreply.github.com>
2026-09-26 21:44:50 -05:00
Brooklyn Nicholson
33f45ca30b fix(gateway): keep one Windows gateway autostart mechanism
A successful Scheduled Task install returned without removing an
existing Startup-folder Hermes_Gateway.vbs or legacy .cmd, and the
fallback path wrote a Startup entry even while a task was still
registered. Both fire at logon, so the gateway launched twice.

install() now removes Startup entries after the task registers, the
fallback is skipped while a task exists, and reconcile_autostart_launchers()
converges an existing install to one mechanism.

Co-authored-by: David Metcalfe <80915+DavidMetcalfe@users.noreply.github.com>
2026-09-26 21:44:50 -05:00
Brooklyn Nicholson
d06a3b8a54 fix(desktop-update): keep console selection from stalling the Windows hand-off
conhost blocks every write to a console while a selection is active. The
hand-off replays buffered child output through Write-HandoffLog after
`hermes update` exits, so a selection in a visible hand-off console held the
result, marker cleanup and relaunch until the user pressed Esc (#103222).

Turn QuickEdit off on the console input buffer for the run (restored on
exit), and skip the console echo while a selection is in progress. The log
file still gets every line.
2026-09-26 21:43:20 -05:00
Brooklyn Nicholson
45fe65498c fix(desktop): spawn the Windows update hand-off wrapper non-detached again
27df3b8847 moved the hand-off spawn into updater/checkout.ts and hard-coded
`detached: true`, dropping the wrapper's own `detached: false`. With
DETACHED_PROCESS the cmd.exe wrapper owns no console, so `start /b` makes
PowerShell allocate a new visible one, and a QuickEdit selection in it
stalls the hand-off before marker cleanup and relaunch (#103222). Spawn the
wrapper the way wrapHandoffForDetachedConsole describes it.
2026-09-26 21:43:20 -05:00
Brooklyn Nicholson
989ddf391e chore: map contributor email for shreyanshj10 2026-09-26 21:43:07 -05:00
Brooklyn Nicholson
21edd6d5ae fix(desktop): stop prescribing repair and antivirus review for an unverified update
A failed receipt check after a zero-exit update means the updated Desktop build
could not be read, not that the install is damaged. The old copy told users to
repair the installation and review antivirus quarantine, which is destructive
advice for a healthy install (#107685). Say what happened and give the one
non-destructive recovery step.
2026-09-26 21:43:07 -05:00
Shreyansh Jain
52329eec83 fix(desktop): stop a stale hand-off root failing a healthy update 2026-09-26 21:43:07 -05:00
Brooklyn Nicholson
831c3d2441 fix(desktop): launch the installed Hermes.app from hermes desktop
On macOS `hermes desktop` launched the release/ bundle even when an
installed Hermes.app existed, so the app ran from two paths (two Dock
icons, TCC grants keyed to a different bundle) while the one Finder opens
stayed stale. Refresh the installed copies first and launch the one that
now matches the checkout build; release/ stays the fallback.
2026-09-26 21:42:48 -05:00
Brooklyn Nicholson
53a785449f fix(update): rebuild Desktop for an installed app with no checkout build
`hermes update` only rebuilt Desktop when apps/desktop/release/ or dist/
existed, so an installed Hermes.app that only the checkout update refreshes
(a bootstrap build) never got newer once release/ was gone or never built.

Count such bundles as a Desktop to keep current. Ownership is read from the
bundle's install-stamp.json: `updateMechanism: self` (or a stamp older than
the field). Self-updating releases and commit builds are never rebuilt or
copied over, and only the checkout an installed app actually runs (the one
under the default Hermes home) claims it. The post-build install now uses
the same set.
2026-09-26 21:42:48 -05:00
Brooklyn Nicholson
a7c080ca66 feat(skills): brag and brag-slim join the optional-skills catalog as upstream stubs
latent-spaces/brag (MIT) turns the project you just built into a short
launch video with music, motion and share copy. It ships two skills:
/brag, the Hyperframes workflow with a bundled music and SFX library,
and /brag-slim, a single SKILL.md where the model builds the whole video
with local tools. /brag hands off to its bundled copy of brag-slim on
Claude Opus 5.5.

Both follow the impeccable/archify pattern: catalog stubs whose
metadata.hermes.upstream pointer makes
`hermes skills install official/creative/<name>` pull the live tree
through OptionalSkillSource._fetch_from_upstream. Nothing is vendored.

The brag stub documents that its Hyperframes path loads HeyGen's
hyperframes-* domain skills by name, and that the skills guard blocks
four of the five today (hyperframes-creative scores dangerous).
brag-slim has no such dependency.

Docs: two generated pages, two catalog rows, two sidebar lines.

Credit: Shunit Haviv Hakimi (shunithaviv), upstream author.
2026-09-26 21:41:31 -05:00
Brooklyn Nicholson
e7b2db974c fix(api-server): deliver transform_llm_output rewrites on streaming routes 2026-09-26 21:39:31 -05:00
cmyyy
7fa45eb349 fix(tui): reap session kernels when a TUI-owned session ends
TUI/Desktop sessions that end via _finalize_session never cleared the
approval session state, so session-persistent execute_code kernels
(owner = approval session key) stayed alive until the NEXT execute_code
in the same process ran the lazy idle sweep in _acquire_kernel.

The gateway's /stop and /new paths already call approval.clear_session
(approval.py:264) at the same boundary, killing the owner's kernels with
the session. Align the TUI path: when the TUI owns the session lifecycle
(not a viewer tab over a gateway-owned session), clear_session(session_key)
on finalize so a finished conversation cannot leak a live interpreter.

Fixes the class: any end_reason that tears down a TUI-owned session
(tui_close, ws_orphan_reap, idle_timeout, lru_evict, tui_shutdown) now
reaps its kernels.
2026-09-26 21:16:13 -05:00
Hermes Agent
0f4a98f87c fix(desktop): pair repeated-caption attachment folds by paste ordinal (#122079)
Two pastes of the same captioned screenshot strip to identical tolerant
captions, so findIndex folded the second paste's error onto the FIRST paste's
settled reply, and the tail prompt match dropped the second paste's prompt as
already-represented by the first's committed row. Pair the n-th local
captioned paste with the n-th stored one; an unpaired ordinal keeps the
conservative preserve path.
2026-09-26 21:05:51 -05:00
Hermes Agent
820bb0b1af fix(desktop): keep the attachment-tolerance dedupe inside the identity gate (#122079)
The tolerant sameAttachmentTurn arm was an unparenthesised || operand, so a
rowId-bearing optimistic row could be swallowed by a committed row it provably
is not — pasting the same captioned screenshot twice dropped the second,
genuine turn. Gate both arms on !conflictingTranscriptIdentity, matching the
comment's invariant; the rowId-less paste keeps matching tolerantly.
2026-09-26 21:05:51 -05:00
Hermes Agent
056a960870 fix(desktop): never tolerance-match a captionless attachment turn (#122079 review)
Two captionless pasted-attachment turns strip to the same empty tolerant
caption, so sameAttachmentTurn matched ANY markers-only stored row against
ANY captionless local row and reconciliation folded one turn's error onto
another paste's reply. Empty now matches nothing: such rows take the
conservative preserve path. Regression test covers the two-paste shape.
2026-09-26 21:05:51 -05:00
Hermes Agent
4da9224467 chore: map contributor emails 2026-09-26 21:05:51 -05:00
Hermes Agent
da4a1ffd08 fix(desktop+gateway): surface continuation kind on compression-chain tips (#121148)
The branch-render guard landed earlier (78b133127d); what remained was
provenance: a projected continuation tip rendered as a brand-new
session. list_sessions_rich now stamps continuation_kind='compression'
on projected rows, StoredSessionRow carries it (contracts regenerated),
and the sidebar session row labels continuations with their lineage
root instead of a fresh-conversation affordance. The live tip stays
listable while naming its parent; sealed chain segments stay hidden.

Co-authored-by: wave-2 worker D <wave2d@hermes-triage>
2026-09-26 21:05:51 -05:00
Hermes Agent
4c1f111de3 fix(desktop): attachment-tolerant transcript reconciliation (#120978)
Pasted-attachment user rows carry no rowId until hydration, so
hydratedIdFor returned undefined and matchesTailUserInNext required
exact text+refs — the conservative append duplicated the turn. Match
on attachment identity (normalized refs + text) when the rowId is
absent, reusing the new attachment-turn helpers.
2026-09-26 21:05:51 -05:00