Commit Graph

21480 Commits

Author SHA1 Message Date
Teknium 085a9d332f
test(desktop): widen HUD composer containment regression coverage (#82319)
Extend the packaged-app HUD geometry test from horizontal-only to full
containment: both axes for the dock and the input, plus an explicit
assertion that no percentage translate survives on the composer dock.
The vertical clipping reported on Windows (#82203) and macOS (#82214)
is the same escape class on the other axis, and the computed-translate
probe makes a future optimizer regression fail with a diagnosis instead
of a bare coordinate mismatch.
2026-08-09 04:03:46 -05:00
Teknium 952f44f841 fix(desktop): focus the update progress window, then hand focus to the relaunched Desktop
Two focus polish items from the first fully-working hand-off run
(ryanc, 2026-08-09):

1. The progress window came up backgrounded: the script is spawned via
   `cmd start /min`, and Form.Show() + TopMost keeps it above other
   windows without ACTIVATING it. Claim activation explicitly
   (Form.Activate + SetForegroundWindow) right after Show.

2. The relaunched Desktop came up behind whatever the user had focused:
   a WMI-spawned process starts unfocused and cannot take foreground by
   itself. Since the hand-off owns foreground while its progress window
   is up, delegate it: AllowSetForegroundWindow(new pid), poll up to 20s
   for Electron's MainWindowHandle, then ShowWindow(SW_RESTORE) +
   SetForegroundWindow. Best-effort at every step -- a focus failure
   never affects the update result.

Sequence on success: progress window foreground during the update ->
window closes -> freshly relaunched Hermes.exe takes foreground.

Verified live on the incident machine: Add-Type shim compiles under
PS 5.1; WMI spawn + AllowSetForegroundWindow + MainWindowHandle poll +
ShowWindow all execute against a real spawned window. (In the bg test
shell SetForegroundWindow returns False by OS design -- only the
current foreground owner may delegate; the real flow's TopMost progress
window IS that owner.) PS parse clean, check-windows-footguns clean.
2026-08-09 02:01:57 -07:00
Teknium 357b97eda6 test(model-metadata): use explicit fixture encodings 2026-08-09 01:51:12 -07:00
Teknium d143bf7a3b fix(model-metadata): resolve provider prefixes from live registry 2026-08-09 01:51:12 -07:00
Oliver Mee 19e51d2cca fix(model-metadata): auto-extend provider prefixes from registered profiles
_PROVIDER_PREFIXES was a hand-maintained frozenset, so providers that ship
as plugins (bundled like fireworks, or user plugins under
$HERMES_HOME/plugins/model-providers/) were never recognised as
provider: prefixes in model strings, and metadata/context-window lookups
received the unstripped string. Mirror the _URL_TO_PROVIDER auto-extend
that already sits below it: add each registered profile's name and
aliases after discovery. The _OLLAMA_TAG_PATTERN guard keeps model:tag
strings intact.

Fixes #66106
2026-08-09 01:51:12 -07:00
Teknium 36eda6112b fix(desktop): detach relaunched Desktop from the hand-off console + UTF-8 child streams
First real-world run of the #82328/#82366 hand-off (2026-08-09, ryanc)
surfaced two defects:

1. The console window never closes after the update finishes -- and
   closing it manually KILLS the freshly relaunched GUI. Root cause:
   Start-DesktopRelaunch spawned Hermes.exe as a child of the console
   PowerShell. Electron/Chromium calls AttachConsole(ATTACH_PARENT_
   PROCESS) at boot, so the new Desktop latched onto the hand-off's
   console: the console can't close while an attached process lives,
   and closing it takes the attached GUI down with it. Fix: create the
   process via WMI (Win32_Process.Create) -- parent becomes WmiPrvSE,
   no console to inherit or attach, same detachment explorer.exe gives
   a normal launch. Start-Process fallback retained (tethered Desktop
   beats no Desktop).

2. Both the console and the progress box render hermes update's UTF-8
   glyphs (checkmarks, arrows) as mojibake. PS 5.1 defaults redirected
   child streams to the OEM codepage. Fix: StandardOutput/ErrorEncoding
   = UTF8 on the child, PYTHONIOENCODING/PYTHONUTF8 so Python emits
   UTF-8, and [Console]::OutputEncoding = UTF8 for our own echo.

Verified live on the incident machine: WMI-created process parents to
WmiPrvSE.exe (not the shell); UTF-8 glyph round-trip through the exact
ProcessStartInfo shape reads back byte-correct (15/15 chars). PS 5.1
parse clean, check-windows-footguns clean.
2026-08-09 01:43:04 -07:00
kshitij 51c07bd8f8 chore: AUTHOR_MAP for drissman@gmail.com (PR #82061) 2026-08-09 14:13:02 +05:30
kshitij f9f4fb4327 test: update systemd scope assertions for start_new_session=True
The #70716 regression fix changes popen_start_new_session from False to
True in the systemd-scope branch.  Update the assertion in
test_wraps_in_systemd_scope_when_supervisor_and_available and the
docstring in test_systemd_post_spawn_failure_never_kills_gateway_process_group.
2026-08-09 14:13:02 +05:30
Driss NAAMANE 0e492a4840 fix(terminal): keep background workers in a private session under systemd scope (#70716)
systemd-run --scope does not give the invoked process a new session: the
worker keeps the parent's session and inherits its controlling terminal.
When the parent is an interactive TUI on a pts (INVOCATION_ID present ->
is_gateway_supervisor_process()=True), every background spawn drops the
worker into the same session as the foreground process group; the spawn
then stops the whole session (SIGTTIN/SIGTTOU family), observed as 5 dead
TUIs in state T ("Arrêté") on 2026-08-08.

Fix: popen_start_new_session = True in the systemd-scope branch of
spawn_local. The worker (and the systemd-run wrapper) get a private
session while the scope cgroup isolation is preserved - the scope is
attached to the invoked process, not to the spawning session.

Verified: simulated TUI (INVOCATION_ID) + ProcessRegistry.spawn_local ->
worker in hermes-worker-*.scope with sid != simulator sid, exits cleanly,
simulator stays alive (previously: same sid -> stopped).
2026-08-09 14:13:02 +05:30
brooklyn! f8bdbc540e
Merge pull request #82373 from NousResearch/bb/drag-title
Drag sidebar sessions and projects by the title
2026-08-09 03:36:07 -05:00
Teknium 45af62cae4 docs: preserve observer version compatibility 2026-08-09 01:30:33 -07:00
Teknium 638ca16af6 docs: correct hook timing semantics 2026-08-09 01:30:33 -07:00
Teknium ad77324559 docs(plugins): catalog shipped hook contracts 2026-08-09 01:30:33 -07:00
Teknium 6495ef82f7 fix(desktop): hand-off hardening - fail-closed gates, truthful completion, progress UI, result surfacing
Review feedback on the #82328/#82366 hand-off, all four points plus the
missing progress GUI:

1. FAIL CLOSED. Both preflight gates aborted-open: a Desktop still alive
   after 30s proceeded anyway, and a shim locked after 20s proceeded
   with --force - both mutate a potentially locked install (the exact
   Access-denied brick class). Now: desktop-alive -> exit 4, nothing
   changed; shim-locked -> exit 5, nothing changed. Both relaunch the
   Desktop so the user is never stranded.

2. TRUTHFUL COMPLETION. `hermes update` treats a Desktop GUI build
   failure as non-fatal (warns, exits 0) - correct for CLI use, a lie
   for a Desktop-driven update that then relaunches the OLD exe as
   "success". The script now detects the warning in the update output,
   retries the build once (`hermes desktop --force-build --build-only`),
   and exits 6 with an honest message when it still fails.

3. MARKER OWNERSHIP. Cleanup now removes the marker only while OUR pid
   still owns it - a handoff partner that rewrote the marker keeps its
   claim (same rule as UpdateLock.release).

4. RESULT SURFACING. The script writes .hermes-update-result.json on
   every exit path (ok, exit_code, message, branch, finished_at). New
   electron/handoff-result.ts consumes it exactly once at the boot
   update-gate: success logs, failure shows a real dialog pointing at
   desktop-update-handoff.log. Stale (>30min) and malformed results are
   consumed silently. Previously a failed detached update was
   indistinguishable from "nothing happened" - the exact live report
   that triggered this work.

5. PROGRESS UI. The old Tauri updater showed a window; the script ran
   in a hidden console with zero feedback. It now shows a WinForms
   progress window (marquee bar + streaming log) pumped via DoEvents
   during the update; -NoUi keeps tests/headless sessions clean, and a
   WinForms-unavailable session degrades to log-only.

Also: subprocess execution moved from Start-Process (ExitCode
unreliably $null under PS 5.1 even with the Handle workaround -
observed live: happy path reported "failed (exit )") to
System.Diagnostics.Process with synchronous stdout pumping, which
keeps the UI alive and the exit code real.

E2E on a real Windows box, sandbox HERMES_HOME + compiled fake
hermes.exe, all five paths:
- happy: exit 0, result {ok:true, "Update complete."}
- shim held open via O_RDWR: exit 5, nothing mutated, honest result
- desktop pid alive (60s ping child): exit 4 after the 30s gate
- update exits 0 printing "Desktop build failed" + rebuild fails:
  exit 6, result names the stale build and the retry command
- foreign-owned marker: overwritten by step-0 claim, removed as owner;
  ownership check verified in the cleanup path
vitest 18/18 (5 new handoff-result tests), typecheck 3 projects clean,
eslint clean, PS 5.1 parse + footguns + ASCII-only clean.

Remaining known gap (deliberate): the full click-to-relaunch lifecycle
through a REAL Desktop build still needs one live Windows verification
after this lands - tracked in the PR body.
2026-08-09 01:26:14 -07:00
Teknium 3b08a0f9b5 fix(desktop): give the update hand-off script its own console - a detached hidden powershell dies before -File runs
Live failure on the first real use of #82328 (2026-08-09): clicking
Update closed the Desktop with "an updater will happen", then nothing.
desktop.log showed `launched repo hand-off script`, but
desktop-update-handoff.log was never created - PowerShell exited 0
without executing a single line.

Root cause, isolated by spawning the exact production shape against a
sandbox HERMES_HOME: `spawn('powershell', [..., '-File', script],
{ detached: true, stdio: 'ignore', windowsHide: true })` kills
powershell.exe during console-subsystem init, before -File processing.
Variant matrix: plain pipes -> runs; hide only -> runs; detached only ->
runs; detached+hide -> exits 0, script never starts. Unit tests and
foreground invocations can't see this class of bug.

Fix: wrapHandoffForDetachedConsole() routes the invocation through
`cmd /d /s /c start "" /min powershell ...` - `start` allocates the
script its own minimized console and fully detaches it; the cmd wrapper
exits immediately. Verified the wrapped form survives the full
detached+hidden production spawn.

Knock-on: child.pid is now the short-lived wrapper, not the script, so
the Electron-side marker pre-write can't represent the script. The
script now claims the update marker itself as step 0 (its own $PID,
byte-exact "<pid>\n<ts>\n" via WriteAllText - Set-Content emits CRLF
and would break the three readers' framing). The Electron pre-write is
kept as a bridge for the spawn window: the script overwrites it, and if
the script never starts the wrapper's dead pid reads as stale and
self-deletes (no wedge). `hermes update` adopts the script's claim via
update_lock.py's process-ancestry rule, unchanged.

E2E in exact production shape (cmd start wrapper, detached, hidden,
parent exits 1.5s after spawn) against a sandbox HERMES_HOME with a
compiled fake hermes.exe: script ran, claimed marker with its own pid
(fake observed "<script-pid>|<ts>|" LF-framed DURING the update),
desktop-pid wait worked, update invoked with correct argv, marker
removed on completion. vitest 13/13 (new wrapper-shape test), 3-project
typecheck clean, eslint clean, PS 5.1 parse + windows-footguns clean.
2026-08-09 01:26:14 -07:00
Brooklyn Nicholson 35b82fdef3 feat(desktop): drag sidebar rows by the title, not just the grabber
The grabber in the lead column was the only way to reorder a session or a
project, and it only appears on hover — a 14px target for the whole gesture.
The row's title is the obvious thing to grab, so put the sortable listeners
on the row shell.

For a session that means two drags share one press, since the title already
starts the drag into the layout. They need no arbitration: each declines
outside its own region. Over the sidebar only the reorder has a target (the
session drop denies — side chrome hosts no main tile); over the tree only the
session drop does (no sortable row there). Whichever the release lands on is
the one that commits.

Rows exclude their own controls through one data-row-actions selector, now
owned by SidebarRowShell instead of restated per row.
2026-08-09 03:23:08 -05:00
Brooklyn Nicholson e128f1c131 fix(desktop): stop lighting drop zones a session can't land in
A dragged session outlined every zone in the tree, including standing side
chrome — the sidebar, files, terminal. None of them host a main tile, so the
drop was always refused; the outline just advertised a target that wasn't one.

Gate the overlay on the same isMainStripPane/isSessionStripPane test that
tileZoneHost resolves the drop with, so what lights up and what commits can
never disagree.
2026-08-09 03:23:00 -05:00
Teknium 26b3918dd9 docs(plugins): clarify tool description sources
Explain that schema.description is model-facing while register_tool(description=...) only populates ToolEntry metadata, and remove the duplicated hello-world description in English and zh-Hans docs.

Refs #60735

Co-authored-by: Shiki <132348332+songshikang0111@users.noreply.github.com>
2026-08-09 00:52:07 -07:00
brooklyn! 934546fd5a
Merge pull request #82345 from NousResearch/bb/home-dupe
Stop /home showing up as a second Home project in the sidebar
2026-08-09 02:38:52 -05:00
hermes-seaeye[bot] 35e562ebd0
fmt(js): `npm run fix` on merge (#82346)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-09 07:36:34 +00:00
Brooklyn Nicholson 41d435ff4d fix(desktop): stop /home showing as a second Home project
/home and /Users hold home directories; neither is a workspace. A session
whose cwd was one of them got promoted to its own auto project, so the
sidebar listed a lowercase "home" row beside the synthetic Home bucket.
Both POSIX spellings are excluded on every host — macOS ships an empty
/home autofs stub, and container/remote shells hand back Linux paths — as
are the filesystem root and the parent of $HOME.
2026-08-09 02:28:02 -05:00
Teknium 92be912d73 feat(desktop): repo-owned Windows update hand-off script - stop depending on the frozen hermes-setup binary
The Desktop's Update button hands off to the staged Tauri binary
(HERMES_HOME/hermes-setup.exe). That binary has no self-update path
(copy_self_to_hermes_home no-ops during --update), so every updater-side
fix only reaches users when a new installer is built, signed, and
published. In practice the published binary lags main by months and
users hit long-fixed bugs on every GUI update: the 2026-08-09 incident
chain was four distinct failures (stale install.ps1 cache resolver
pre-#67369, marker adoption pre-#74782, straggler teardown) all caused
by a June 4 binary running against an August repo.

This inverts ownership: scripts/desktop-update.ps1 lives in the repo
checkout, so every `hermes update` refreshes the code that drives the
NEXT update. Only PowerShell itself - an OS component - stays frozen.

Desktop side (apps/desktop/electron):
- resolveUpdateScriptHandoff() (updater-process.ts): returns the spawn
  recipe when scripts/desktop-update.ps1 exists in the checkout;
  Windows-only (POSIX updates in place via applyUpdatesPosixInApp);
  null on old checkouts -> caller falls back to the staged binary path
  completely unchanged.
- applyUpdates() prefers the script hand-off. The marker pre-write is
  ALWAYS safe on this path - no stagedUpdaterSupportsPrewrittenMarker()
  mtime heuristics - because hermes_cli/update_lock.py's UpdateLock
  adopts a live marker held by a process ANCESTOR, and the script is
  the `hermes update` child's parent. This closes the unguarded
  marker-gap window that pre-#74782 binaries force today (the 23:56
  failure in the incident: 'skipping marker pre-write: staged updater
  predates self-adopt' -> renderer respawned a backend into the gap ->
  update refused).
- CLI-installed users (no staged binary) now get the script hand-off
  too instead of the manual `hermes update` card, when the script
  exists.

Script (scripts/desktop-update.ps1): waits for the Desktop pid to exit
(bounded 30s), waits for the venv shim to unlock (mirrors the Rust
is_locked probe, bounded 20s), runs `hermes update --yes --gateway
--force --branch <ref>` from the CURRENT checkout with one retry for
the update-boundary class (skipped for exit 2), removes the marker on
every exit path, relaunches the Desktop. ASCII-only (the #67193
lesson), logs to logs/desktop-update-handoff.log.

Verification (real Windows box):
- apps/desktop: typecheck (3 projects) clean, eslint clean, vitest
  updater-process.test.ts 12/12 (3 new resolver tests).
- Script E2E against a sandbox HERMES_HOME with a compiled fake
  hermes.exe: correct argv (update --yes --gateway --force --branch
  main), stale marker removed, exit code propagated (0 and 1 paths),
  retry-once fires exactly once on failure, PS 5.1 parse + windows
  footguns check clean.
- Contract E2E with the real UpdateLock: ancestor-owned marker adopted
  (True), left in place on release, foreign live holder still refused.
2026-08-09 00:27:06 -07:00
kshitij 61515e8116 fix: update legacy draft test for new supports_draft_streaming gate
The existing test asserted supports_draft_streaming returns True with
rich_messages=True and rich_drafts=False, but the PR's gate now makes
it return False. The test already force-sets _use_draft_streaming=True,
so the assertion was redundant — updated to reflect the new behavior.

Also removed redundant manual attribute overrides in test 3 where
_make_adapter(extra={'rich_messages': False}) already sets the flag.
2026-08-09 12:53:45 +05:30
Slobaka 3332ad4dbf fix(telegram): avoid MDV2 draft preview when rich_messages lacks rich_drafts
When rich_messages is on and rich_drafts is off, transport=auto used
sendMessageDraft (MarkdownV2 tables→bullets) then finalized via
sendRichMessage. Users saw a crooked first bubble and a second wiki-style
final. Decline drafts in that config so auto uses edit-in-place + rich
finalize on one message.

Fixes #78524
2026-08-09 12:53:45 +05:30
Teknium 471baea520 feat(plugins): map portable Agent Plugins streamable-http entries into the native MCP runtime
Agent Plugins v1 packages with 'streamable-http' mcp.json entries now load
through Hermes' existing URL-based MCP client instead of being reported and
skipped. The stdio-only limitation was the agreed follow-up slice from
PR #81196.

Boundary rules from the v1 spec (§7.2.1) are enforced:
- URL must be absolute http(s), no user information, no fragment; plain
  HTTP only for localhost/loopback hosts.
- Configured package headers are never forwarded across a cross-origin
  redirect: translation marks entries strict_redirect_headers, and the
  redirect hook in the native runtime strips those headers (plus
  Authorization) whenever a redirect leaves the original origin. On mcp <
  1.24.0, where the client cannot hook redirects, such servers fail closed
  with an actionable upgrade message.
- Legacy 'sse' entries remain reported and skipped.

The redirect hook is extracted into a testable module-level factory
(_make_redirect_header_stripper); default behavior for native config
servers is unchanged (Authorization-only stripping).
2026-08-08 23:56:46 -07:00
Teknium 961f7481a7 fix(relay): bypass managed execution for nested calls inside managed callbacks
The native Relay pipeline binds its Futures to the event loop that
entered run_in_session_async. While a managed tool callback executes,
that loop is blocked until the callback returns — so a nested managed
relay call made from inside the callback (vision_analyze's auxiliary
LLM call on a worker-thread loop) awaits a Future that can never
complete: 'RuntimeError: Future attached to a different loop', or a
deadlock, plus 'Event loop is closed' at shutdown when the orphaned
future completes late. (#77244)

Fix: managed_callback_guard, a ContextVar depth marker set around every
Hermes callback the relay adapters hand to the native pipeline
(relay_tools.execute invoke, relay_llm execute/execute_async invoke,
ManagedLlmStream run_callback). resolve_execution_context returns the
no-relay triple while the marker is set, so nested calls run unmanaged.
The marker propagates through contextvars.copy_context() into the
worker threads tools use for their internal async work.

Top-level turn LLM calls and tool wraps stay fully managed — verified
live: vision_analyze works under active shared metrics while the main
turn still records managed llm.execute events.

Alternative fixes considered and rejected: removing
retain_managed_execution (kills the shared-metrics managed pipeline)
and gating on main-thread identity (managed tool wraps legitimately
run on the run_agent thread, so that gate disables relay everywhere).
2026-08-08 23:46:17 -07:00
brooklyn! 62431364e3
Merge pull request #82233 from kerpopule/fix/desktop-hud-composer-clipping 2026-08-09 01:24:08 -05:00
teknium1 bb8280b753 revert(desktop): roll Electron back to 40.10.2
Reverts the Electron portion of 7537de9e74 (40.10.2 -> 40.10.6,
GHSA-r4w5-6pfg-jxp5). The bump broke fresh Windows installs: 40.10.3+
swapped install.js's extraction to @electron-internal/extract-zip, an
MSVC-built native binding that ERR_DLOPEN_FAILEDs on machines without
the VC++ Redistributable (field report Aug 9, fresh VM, confirmed).

Security impact of reverting is nil for our code paths:
- GHSA-r4w5-6pfg-jxp5 (Moderate 5.9): requires the legacy
  ProtocolResponse.url API with session-partition isolation; desktop
  only uses the modern protocol.handle() and no cross-session
  isolation. Not exposed.
- GHSA-9f4c-93c8-jc8g (High 7.2): affects ALL of 40.x with no fixed
  40.x release (fix is 41.10.3+); 40.10.2 vs 40.10.6 is a wash. Real
  mitigation is hardening our setWindowOpenHandler (follow-up PR).

allowScripts version-keyed entry moved to electron@40.10.2 so the
postinstall (dist download) still runs; allow-scripts-sync vitest
suite passes. Electron 41.x major remains deferred/on hold.
2026-08-08 23:24:02 -07:00
Teknium ceebb21dd7 fix: suppress pydantic serializer warnings leaking to the terminal
The Anthropic SDK's streaming accumulator builds ParsedMessage snapshots
whose ParsedTextBlock content doesn't match the generic union pydantic
expects, so model_dump() on stream events (message_stop) emits
PydanticSerializationUnexpectedValue UserWarnings straight into the
user's CLI output mid-response.

Pass warnings=False at every helper that dumps arbitrary SDK models
(relay_llm/_jsonable, relay_tools/_jsonable, anthropic_adapter
_to_plain_data, run_agent _hook_jsonable, chat_completion_helpers
extra_content/reasoning_details sites, chat_completions transport),
with a TypeError fallback for duck-typed model_dump implementations.

Adds regression tests including a precondition test that proves the
fixture still trips the warning without suppression.
2026-08-08 23:14:30 -07:00
Teknium 1d45e62f30 fix(install): replay npm's debug log into the bootstrap stream on failure
On Windows npm prints only a terse summary on failure; the actual cause
(postinstall stderr like Electron's install.js, network traces, EBUSY
retries) lives in npm-cache\_logs\<ts>-debug-0.log, which never reached
the Tauri bootstrap log. Field report: a fresh-VM desktop install died
with 'npm error command node install.js' and zero actionable detail.

Adds Write-NpmDebugLogTail: locates the debug log from npm's 'A complete
log of this run' line (fallback: newest _logs/*-debug-*.log under 'npm
config get cache') and replays its last 200 lines through our output
stream, which the bootstrap installer's streaming sink captures.

Wired at all four npm failure sites: desktop workspace npm ci/install,
_Run-NpmInstall (browser tools), Install-AgentBrowser (--silent global
install), and the desktop 'npm run pack' build step.
2026-08-08 22:55:00 -07:00
Teknium 3a915c46d3 fix(update): scope bootstrap-cache refresh to the update-target ref, match installer pin rules
Two cache-key correctness follow-ups to #82229 (review feedback):

1. Abbreviated commit pins are immutable too. The installer's
   is_valid_commit() accepts 7-40 hex chars, but the Python refresh
   exempted only exactly-40-hex names — an abbreviated pin like
   install-4ce1994.ps1 could be overwritten with a branch script. The
   predicate now mirrors the Rust rule (7-40 hex = immutable, never
   rewritten), applied to the sanitized target ref.

2. Refresh only the update-target ref's cache key. The helper rewrote
   EVERY mutable-ref entry with the active checkout's script: with
   install-main.ps1 and install-bb_gui.ps1 coexisting, updating main
   replaced both with main's script — cross-branch cache poisoning in
   the other direction. It now computes the single cache key for the
   branch being updated, using the installer's own ref sanitization
   (sanitize_ref: non [A-Za-z0-9._-] -> '_', so bb/gui ->
   install-bb_gui.ps1), and touches nothing else. Entries the
   bootstrapper never wrote are not created.

The branch is threaded from the existing `branch =
_resolve_update_branch(args)` in both _cmd_update_impl call sites and
_update_via_zip (main-only by its own guard).

Regression tests lock down both invariants: abbreviated-SHA pin
untouched (including when passed as the branch), coexisting mutable
refs (main refresh leaves install-bb_gui.ps1 byte-identical),
sanitize_ref parity, and uncached-ref no-op.

E2E on the incident machine's real bootstrap-cache: planted a stale
install-main.ps1 + sibling install-bb_gui.ps1 + abbreviated pin
install-4ce1994.ps1; refresh("main") healed main byte-exact and left
both others untouched; refresh("4ce1994") was a no-op. The pre-existing
40-hex pin entry in the real cache was also untouched.
2026-08-08 21:18:09 -07:00
Teknium 3dcbe9001f fix(update): refresh the installer's bootstrap-cache scripts on every update
Pre-#67193 hermes-setup binaries (June 2026 and earlier, including the
newest published build) resolve bootstrap-cache/install-<branch>.ps1 by
"exists -> reuse forever": a branch-ref cache entry written at install
time is never re-downloaded, so every GUI update/repair executes a
months-stale install script. The binary has no self-update path, so no
amount of `hermes update` fixes it.

Live incident (2026-08-09, ryanc): install-main.ps1 cached June 4 lacked
the #81327 venv process-tree sweep; the bootstrap venv stage died with
"Cannot remove item venv\Scripts\python.exe: Access denied" on a
straggler backend pair, twice, despite every relevant fix already being
merged on main - the installer simply never ran that code.

Fix: `_refresh_bootstrap_cache_scripts()` runs at the end of every
update path (git, zip, already-up-to-date repair), overwriting mutable
branch-ref cache entries with the freshly pulled scripts/install.ps1 /
install.sh. The stale binary's unconditional reuse becomes a feature: it
"reuses" a file the update keeps permanently current. Post-#67193
installers re-download on every run anyway, so this is a harmless
pre-seed of identical bytes for them.

Scope guards: 40-hex commit-SHA entries are immutable pins and are never
touched; .ps1 gets the UTF-8 BOM to match the installer's cache format
(#67193); best-effort - a failed refresh never fails the update.

E2E on the incident machine: poisoned the real
bootstrap-cache/install-main.ps1 with a stub, ran the real function -
healed byte-exact to the checkout's script (BOM intact, #81327 tree-kill
sweep present).
2026-08-08 20:54:45 -07:00
Steve Darlow f2731da4a4 fix(desktop): keep HUD composer within window 2026-08-08 22:54:16 -05:00
hermes-seaeye[bot] 1792e756e4
fmt(js): `npm run fix` on merge (#82209)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-09 03:32:52 +00:00
brooklyn! 4b601931be
Merge pull request #82201 from NousResearch/bb/xplat
Fix the desktop app's Linux gaps: window geometry, HUD click-through, and window-read diagnostics
2026-08-08 22:25:02 -05:00
mzkarami 9eec86923c docs: align Ollama tool-calling guidance 2026-08-08 20:21:30 -07:00
Brooklyn Nicholson 04afc8d48e fix(desktop): say why read_window_below cannot see the windows
When enumeration was impossible the tool answered "could not determine the
window underneath (the desktop app did not answer, or window enumeration is
unavailable on this system)" — true, and a dead end. On Linux the two ways it
fails have opposite fixes and neither is guessable from that: a Wayland session
withholds window identity from applications outright, while an X11 session
needs xprop and xwininfo installed, because that is what the enumerator shells
out to.

Answer with the reason instead of nothing. A session with both WAYLAND_DISPLAY
and DISPLAY is XWayland, where xprop can still answer, so it gets the tooling
advice rather than being told to change session type.
2026-08-08 22:17:38 -05:00
Brooklyn Nicholson da933bf279 fix(desktop): keep the HUD clickable on Linux
Click-through decides whether to swallow the mouse by hit-testing the document
under the cursor, and it learns where the cursor is from mousemove. Those keep
arriving while the window ignores the mouse only because of
`setIgnoreMouseEvents(true, { forward: true })`, and `forward` is
`@platform darwin,win32`. On Linux the moves stop the instant the HUD turns
click-through, so it never sees the pointer return to the bar: the bar is
visible, and clicking it hits whatever is behind.

Main can still see the cursor, so on Linux it polls and pushes the position to
the renderer, which runs its usual hit test on it. The decision and its rules
stay in one place — only the courier for that one input changes — and off-window
is sent as null, which is already how the renderer hands the mouse back.
2026-08-08 22:15:24 -05:00
Brooklyn Nicholson d73bc7f17a fix(desktop): persist window geometry on Linux
`moved` and `resized` are macOS/Windows only — Electron tags them
`@platform darwin,win32` — so on Linux neither the main window nor the HUD
ever heard that it had been dragged or resized, and both reopened at their
default placement every launch. The main window had a `close` flush to fall
back on; the HUD had nothing, so its position was lost outright.

Bind `move`/`resize` instead. Those carry no platform tag and fire everywhere,
and the trailing debounce already collapses the mid-drag stream a settled event
would have saved us from.
2026-08-08 22:13:08 -05:00
Teknium da3a0a852f fix(update): make orphan-backend reap tree-aware + drain Desktop update trees without pre-signalling
Follow-up to #82179 addressing helix4u's review comment
(#82179 issuecomment-5229441571). Three parts:

1. Desktop teardown (salvaged from #77436, @4adwentures): the update
   hand-off's releaseBackendLock() sent SIGTERM to the primary backend
   BEFORE taskkill /T. If the launcher exits first, Windows can no longer
   enumerate its descendants and they survive holding the venv — the
   Electron path that creates the orphan #82179 then has to repair.
   New stopBackendTreesForUpdate() tree-kills the live root first, with
   the behavioral vitest from #77436. The scanner half of #77436 is
   deliberately NOT taken (superseded by #82158's full-cmdline scan).

2. Tree-aware orphan classification: _orphaned_desktop_backend_pids()
   previously refused the whole holder set when any holder had a live
   parent. But the scanner legitimately returns an orphaned serve root
   AND its descendants (the venv trampoline's uv-managed interpreter
   worker — which carries the same backend argv — plus .hermes-runtime
   children). Those have a live parent: the orphan root itself. Now
   holders inside an accepted orphan root's tree fold into that root
   (only roots are returned; taskkill /T reaps descendants), and
   live-parent backends defer to the ancestry check instead of refusing
   outright. Anything outside an orphan tree still refuses.

3. Tests for the mixed shapes: root+managed-runtime child,
   grandchild depth, non-descendant stray alongside an orphan root
   (still refuses), descendant exited mid-classify.

E2E on a real Windows box: spawned a detached backend-shaped orphan
that itself spawned children (3 python descendants); the scanner-shaped
mixed holder set classified to [root], taskkill /T reaped root and all
descendants. The live Desktop backend on the box still classified None
(refusal preserved). The first E2E attempt caught exactly the
trampoline/worker case the mocks missed — the live worker re-execs with
the same backend argv and a live parent — which is what part 2 fixes.

Co-Authored-By: 4adwentures <296413879+4adwentures@users.noreply.github.com>
2026-08-08 20:01:03 -07:00
Teknium 826bf9b6d8 fix(update): reap orphaned Desktop backends instead of dead-ending the venv-holder guard
The GUI-updater handoff race: the Desktop fires SIGTERM + app.quit() and
spawns hermes-setup, but its Python backend (`python.exe -m
hermes_cli.main serve`) can survive the teardown. The Desktop is gone --
nothing will respawn that backend -- yet the venv-holder guard refused on
it and the update dead-ended with "Hermes is still running" while the
user had zero windows open (observed twice on 2026-08-09, 01:59 and
02:17, bootstrap-installer.log).

New `_orphaned_desktop_backend_pids()` classifies remaining holders: a
serve/dashboard backend whose supervising parent is provably dead (PID
gone, or recycled -- parent created after the child) is a straggler safe
to reap. Any live-parent backend, non-backend holder, or unprovable case
keeps the refusal exactly as before. Reaping uses the new
`_stop_process_trees()` (taskkill /T /F), mirroring the Desktop's
forceKillProcessTree and install.ps1's venv sweep so the managed
.hermes-runtime interpreter child dies with its launcher (#70026).

Builds on #81327 (salvaged intact underneath): that fixed the same
parent-only-kill gap in install.ps1's venv sweep; this closes the
remaining dead-end in the `hermes update` guard itself.

E2E on a real Windows box: spawned a detached orphan with a
backend-shaped argv -> classifier returned its PID and the tree reap
killed it; a non-backend orphan and the live Desktop backend (parent
alive) both returned None (refusal preserved).
2026-08-08 19:46:35 -07:00
Gille a09124cea5 fix(install): stop managed runtime child trees on Windows 2026-08-08 19:46:35 -07:00
brooklyn! 9c8a2352f7
Merge pull request #82171 from NousResearch/bb/allowscripts-sync
fix(build): unblock Windows desktop builds — allowScripts drift + get-windows self-heal
2026-08-08 21:29:25 -05:00
Brooklyn Nicholson a692393da7 test(build): hold allowScripts in sync with the lockfile
Both halves of this bug were the same failure mode: allowScripts is keyed by
exact name@version, so an entry stops matching the moment a dependency moves
and npm demotes the blocked script to a warning nobody reads. The breakage
surfaces much later as a missing native artifact on one platform.

Assert the two relationships that make the allowlist meaningful — every
versioned pin resolves to a version the lockfile installs, and every package
the lockfile marks as having an install script carries a decision. A
bare-name key stays exempt from the version check so a standing denial like
unicode-animations survives bumps.

Lives in tests-js because the CI change classifier does not run the Python
suite for a manifest-only diff.
2026-08-08 21:23:25 -05:00
Brooklyn Nicholson 2cd9e1777b fix(desktop): rebuild get-windows when its win32 binding is missing
Fixing the allowlist only helps a fresh install. npm will not re-run an
install script for a package already on disk, so every checkout that
installed while get-windows was blocked stays bricked: `hermes update`
pulls the fix, `npm install` skips the script, and the build fails on the
same missing binding.

Run `npm rebuild get-windows` from the staging step when the binding is
absent, and if that still yields nothing, print the two commands that
recover the checkout by hand instead of the previous advice to reinstall
dependencies, which is exactly what the user already tried. Gated to a
win32 host building for win32, since no other host can produce the binding.

Co-authored-by: JoaoMarcos44 <JoaoMarcos44@users.noreply.github.com>
2026-08-08 21:23:20 -05:00
Brooklyn Nicholson 7210db5646 fix(build): allow get-windows install script, refresh stale allowScripts pins
get-windows was added to apps/desktop without a root allowScripts entry, so
npm blocked its node-pre-gyp install script and the win32 prebuilt binding
was never downloaded. Every Windows desktop build then died in
stage-native-deps, including the updater's headless rebuild, leaving Windows
users unable to update the app.

The same manifest had drifted twice more: the CVE sweep in 7537de9e74 moved
Electron to 40.10.6 and left the electron@40.10.2 pin behind, and website's
allowlist still names core-js-pure, which its lockfile no longer resolves,
while fsevents runs an install script with no entry at all.

Co-authored-by: gsy324 <gsy324@users.noreply.github.com>
Co-authored-by: elbukott1 <elbukott1@users.noreply.github.com>
Co-authored-by: Brian Franco <BrianFranco@users.noreply.github.com>
2026-08-08 21:23:16 -05:00
Teknium 851f23ebc6 fix(cli): fence OSC 11 background query with DA1 so late replies can't leak into the prompt
The classic CLI's light-mode detection sends an OSC 11 background-color
query and blind-waits 100ms. Terminal managers that swallow OSC 11
(herdr) made every startup pay the full 100ms for nothing, and any
in-order relay that answers slower than 100ms (SSH bridges, WSL,
loaded tmux servers) delivered the reply AFTER prompt_toolkit owned
the tty — the rgb:.../escape payload leaked into the input line as
gibberish characters.

Fix: send the OSC 11 query followed by a DA1 sentinel (ESC [ c) in one
write — the same fence pattern the Ink TUI's TerminalQuerier uses.
Terminals answer queries in order and effectively all of them answer
DA1, so the DA1 reply proves the terminal has already processed (or
ignored) our OSC 11. Fast terminals and herdr-style multiplexers now
resolve in ~1ms; slow relays get their reply consumed instead of
leaked; a hypothetical DA1-mute terminal falls back at a 1s safety
net, same clean timeout path as before.

Adds real-PTY regression tests covering the herdr-style (DA1-only),
slow-relay (+300ms reply), and fully mute emulator behaviors, each
asserting zero leftover bytes in the tty buffer. Sabotage-verified:
the slow-relay test fails against the old un-fenced code with the
exact leak payload in LEFTOVER.
2026-08-08 19:22:53 -07:00
liuhao1024 54641186ff fix(cli): drain late OSC 11 replies after TCSAFLUSH to prevent input leak
On slow terminals (VPS, containers under load), the OSC 11 background
color response can arrive after TCSAFLUSH completes — leaking into
prompt_toolkit's input buffer and silently consuming the first 1–3
characters of every response.

Add a 50ms post-flush drain window that reads and discards any late
bytes via select() + os.read() before prompt_toolkit grabs the tty.

Fixes #40250
2026-08-08 19:22:53 -07:00
Teknium 0b17b691d6 fix(gateway): skip attachment upload for failed first turns in queued delivery
Adds the failed-result guard the salvage review called for:
_deliver_queued_first_response now takes deliver_media and the queued
follow-up call site passes deliver_media=not _delivery_result.get('failed').
A failed turn still delivers its normalized failure text (pinned by
test_run_agent_sends_normalized_failure_before_queued_followup), but its
attachments are no longer uploaded as if the turn succeeded — mirroring
the completed-turn path's 'not agent_result.get(failed)' guard.
Regression test added.
2026-08-08 19:17:13 -07:00
StellarisW a52dd17d93 fix(gateway): preserve queued media continuity 2026-08-08 19:17:13 -07:00