Commit Graph

28271 Commits

Author SHA1 Message Date
MaxFreedomPollard eeaba3a88d fix(gateway): do not claim a destructive-slash opt-out that was not saved
Answering "Always Approve" on the /clear, /new, /reset and /undo
confirmation calls save_config_value("approvals.destructive_slash_confirm",
False) and then appended "Future /clear, /new, /reset, and /undo will run
without confirmation" unconditionally.

save_config_value catches its own exceptions and reports the outcome in the
return value, so the caller's try/except could never observe a failed write,
and the return value was ignored. On any install whose config.yaml is not
writable the user was told the preference stuck when it had not, and the
prompt returned on the next restart with no explanation.

Check the return value. The approved action still runs either way, but when
the write did not land, say so and point at the config key instead of
promising an opt-out that was never written.
2026-07-31 22:35:03 -07:00
MaxFreedomPollard 65e9ece964 fix(curator): restore the real skills tree when a rollback extract dies part-way
shutil.move() moves into an existing destination directory instead of
replacing it. When the snapshot extract failed after creating some of its
output, the recovery path moved each staged entry onto a path the extract
had already created, burying the user's own skill one level deeper
(skills/alpha/alpha/) and leaving the snapshot's partial content in its
place. rollback() then returned "snapshot extract failed (state restored)".

Clear the failed extract's output before moving the staged copies back:
entries the original tree never had are dropped, and each staged entry's
destination is removed before the move. When an entry still cannot be
restored, keep the staging dir and name the entries in the message rather
than reporting a restore that did not happen.
2026-07-31 22:34:57 -07:00
MaxFreedomPollard 31032b4f51 fix(auth): a transient read failure is not corruption
_load_auth_store() treated every exception from reading auth.json as
corruption and returned an empty store. EMFILE under fd exhaustion,
EACCES, EIO and a stalled network mount all reached that branch. This
module does read-modify-write in roughly fifteen places, so the empty
store was one _save_auth_store() away from erasing every stored
credential.

Separate OSError from parse failure: a file that exists but cannot be
read now raises, naming the real cause and leaving the file on disk
untouched. Only a genuine parse failure takes the preserve-and-start-
empty branch, which is unchanged.

The backup was also unreliable in exactly the conditions that triggered
it: shutil.copy2 opens a file, so under EMFILE it failed too, its bare
except swallowed that, and the log still said "Corrupt file preserved
at ..." when nothing had been written. Track whether the copy landed
and say so accurately.
2026-07-31 22:34:52 -07:00
iso2kx d358edd916 fix(update): snapshot venv launchers before the gateway drain
_venv_launcher_ancestors() ran after
_wait_for_windows_update_gateway_exit(), but the drain stops tracking a
PID exactly when it dies - for the common graceful-drain case the worker
is gone by the time the wait returns, and a dead pid's parent cannot be
recovered, so the launcher stop never fired on that path. Resolve
launcher ancestors before draining and stop the snapshot afterwards
alongside the survivors; a launcher that already exited with its worker
raises ProcessLookupError at the kill and is skipped.

The set-cover invariant test now marks drained workers uninspectable
(construction raises, like psutil.NoSuchProcess), so a post-drain
launcher lookup can never reappear unnoticed.
2026-07-31 22:34:28 -07:00
iso2kx f3edd0e538 fix(desktop): use the canonical gateway matcher for the preflight exemption
_is_pausable_gateway() hand-rolled a second gateway parser and regressed
a valid form: in `--profile gateway gateway run` the profile VALUE
shadowed the subcommand token, so the scan reported that gateway as a
fatal preflight holder. Delegate to
gateway.status.looks_like_gateway_command_line() - profile-selector
aware, shlex-tokenizing, run-only - so the preflight exemption, the
pause discovery, and the updater's guard fallback share one parser.
Non-run gateway subcommands, serve backends, and REPLs still block; the
bare-`gateway` form now classifies as a running gateway, mirroring the
canonical matcher's contract.
2026-07-31 22:34:28 -07:00
iso2kx a31fe8db6e fix(update): stop gateway holders the guard finds after the pause
The pause stops every gateway its discovery maps, but the venv-holder
guard sees the process table as it is now: a gateway respawned by its
supervisor (Scheduled Task, login watchdog) inside the pause-to-guard
window, or one started through a spawn path discovery does not map,
still holds venv .pyds - and the guard dead-ended the update on exactly
the kind of process the pause machinery exists to stop.

When every remaining holder classifies as a pausable gateway - using the
same _is_pausable_gateway matcher the Desktop preflight uses, so the two
views cannot drift - stop them and re-scan once. Any non-gateway holder
(REPL, stray script, Desktop backend) keeps the hard refusal exactly as
before, and a survivor after the stop still aborts.
2026-07-31 22:34:28 -07:00
iso2kx 0bec37aefc fix(desktop): don't report pausable gateways as venv-update blockers
The Desktop update preflight (`scanVenvBlockers` -> `python -m
hermes_cli._scan_venv_blockers`) reports every venv-side python as a
blocker and aborts the handoff:

    main.ts: scanVenvBlockers(...)                  <- aborts HERE
             return { ok:false, error:'venv-blocked' }
             spawnUpdaterProcess(hermes-setup ...)  <- never reached

But a *gateway* is not a dead-end holder. `hermes-setup` invokes
`hermes update --yes --gateway`, and the CLI updater's
`_pause_windows_gateways_for_update()` gracefully drains and stops
running gateways before touching the venv — machinery added for exactly
these processes (#50090 and follow-ups). The preflight replicated the
CLI's *guard* without its *pause*, so a Windows service-mode gateway
(e.g. a Scheduled Task running `gateway run`) made every Desktop update
abort forever with

    [updates] venv-blocked: N process(es) hold the install
      PID ... python.exe ... -m hermes_cli.main gateway run --replace

while the component one layer down was never allowed to run and handle
it. The abort points at a process the updater knows how to stop.

Fix: `_is_pausable_gateway()` exempts `hermes_cli.main ... gateway run`
invocations (both halves of the venv-shim launcher/worker chain match,
since the uv-side worker re-runs the same argv). Everything else keeps
blocking — the Desktop `serve` backend, other `gateway` subcommands,
operator REPLs and stray scripts have no pause machinery downstream.
The CLI updater's own post-pause venv guard is untouched, so a pause
that genuinely fails still aborts before any .pyd mutation.

The JSON gains a diagnostic `pausable_gateways` count. The TS consumer
validates only `ok`/`blocked`/`processes` and ignores unknown keys, so
old and new Desktop builds both accept the new document; no Electron
rebuild is required for the fix to take effect (the scan runs from the
repo's Python).
2026-07-31 22:34:28 -07:00
iso2kx 05504bd9f0 test(gateway): make test_gateway collectable on Windows
`import pty` at module scope pulls in `termios`, which does not exist on
Windows. That raised ModuleNotFoundError during *collection*, so pytest
aborted the whole module with

    Interrupted: 1 error during collection

before any skip marker could take effect. The single PTY-dependent test
was already correctly marked `skipif(sys.platform == "win32")` — the
import crashed ahead of it and took the module's 13 other, entirely
platform-agnostic tests down as collateral. Windows contributors got zero
gateway coverage and, worse, a collection error that masks real failures
in any batch that includes this file.

Two changes:

- Move `import pty` into the `stdin_is_tty` branch that actually uses it
  (the sole `pty.openpty()` call). Nothing else in the module needs it.
- Skip `test_systemd_install_checks_linger_status` on Windows. It drives
  `_systemd_linger_enabled()` -> `os.getuid()`, which does not exist on
  Windows; the production helper is annotated
  "windows-footgun: ok — POSIX systemd helper, never invoked on Windows",
  so the test is Linux-only by nature. It was previously hidden behind
  the collection crash.

POSIX behaviour is unchanged: both guards are `skipif(win32)`, inactive
off Windows, and the local import resolves exactly where the module-level
one did.

    before (Windows): 0 collected, 1 collection error
    after  (Windows): 14 collected, 9 passed, 5 skipped
2026-07-31 22:34:28 -07:00
iso2kx 9507f4382e fix(update): stop the venv-side launcher of each paused Windows gateway
On Windows a gateway started through the venv shim is a two-process chain:

    venv\Scripts\python.exe        (launcher — keeps venv .pyd files mapped)
      └─ uv\python\...\python.exe  (worker  — writes the gateway PID file)

`_pause_windows_gateways_for_update()` builds its pause set from
`find_gateway_pids()`, which reads the PID file and therefore only ever
sees the *worker*. The venv-holder guard immediately downstream
(`_detect_venv_python_processes()`) matches on the venv path prefix, so it
only ever sees the *launcher*.

The two sets are disjoint. A gateway the updater had just gracefully
drained still left its launcher alive, the guard reported that launcher as
a venv holder, and the update aborted — every time. On the Desktop path
this surfaces as the dead-end dialog:

    [updates] venv-blocked: 2 process(es) hold the install
      PID ...  python.exe  ...\venv\Scripts\python.exe -m hermes_cli.main gateway run --replace

Note the reported holder is a gateway the updater believes it stopped.
The Desktop path is affected because `hermes-setup.exe` runs
`hermes update --yes --gateway --force`, and `--force` deliberately does
NOT bypass the venv guard (that needs `--force-venv`), so the abort is
correct behaviour reacting to an incomplete pause.

Fix: after the graceful drain, walk one hop up from each mapped gateway
PID and force-kill parents that live under the project venv.

Deliberately additive, not a substitution:

- The planned-stop marker and the graceful drain still target the worker
  (the PID that wrote the PID file), so clean shutdown is unchanged and
  updates don't get pushed onto the hard-kill path.
- `terminate_pid(force=True)` is `taskkill /T` (tree kill), so killing a
  launcher that outlived its worker also reaps stragglers.
- `_resume_windows_gateways_after_update()` needs no change: the mapped
  respawn argv is rebuilt from the profile name
  (`_gateway_run_args_for_profile`), never from the killed PID, and the
  restart watcher's `_pid_exists()` wait still terminates because the
  tree kill takes the whole chain down.
- Only the venv-side parent is returned. Unrelated ancestors (a Scheduled
  Task's `cmd.exe`, an operator's shell) are ignored, and the caller's own
  process chain is excluded so a CLI `hermes update` never nominates
  itself.

Tests assert the invariant the two PID-resolution paths must satisfy —
the pause's kill set must cover the guard's abort set — rather than
snapshotting PIDs. Verified to fail without the fix:

    AssertionError: pause stopped [] but the venv guard aborts on [400]
    — disjoint sets abort the update
2026-07-31 22:34:28 -07:00
Teknium f9522fdcef chore: map salvage contributor emails (zakhounet, Kewe63) 2026-07-31 22:33:53 -07:00
Kewe63 16e66e721f fix(desktop): break renderer-led reinstall loop on transient backend stalls
Issue #74874. The renderer's 'Repair' button treated every transient
backend GIL stall (event loop stalled ... ws ready frame send failed)
as a fatal backend fault, asking the bootstrap to force-reinstall +
restart, which then stalled again for the same reason — looping the
user through 30+ minutes of reinstall cycles.

Distinguish 'venv is genuinely broken' from 'backend is just transiently
stalled' before honouring a repair request. Probe the live backend
process (exitCode === null && signalCode === null) and an in-flight
repair-attempt counter:

  attempt <= 3 AND primary alive   → soft restart (skip installer)
  attempt <= 3 AND primary dead    → soft restart (verify before reinstall)
  attempt >  3                    → hard reinstall (escalate)

Counter resets on a clean backend.ready so a later, unrelated failure
episode starts at attempt 1. The guard is a pure helper (decideBootstrap
Repair in electron/bootstrap-repair-guard.ts) so the decision logic is
unit-tested in isolation; main.ts only wires the existing flag and
counters to it.

Refs #74874
2026-07-31 22:33:53 -07:00
zakhounet 0ee9723b52 fix(desktop): make the non-Windows updater bypass an explicit policy
resolveUpdaterBinary() picked up a staged hermes-setup on every platform, so a
macOS binary predating the update hand-off protocol took over the update, held
the marker, and had its `hermes update` child refuse its own parent. The in-app
Update button then failed for good, with no route -- update, re-download or
reinstall -- back to a capable binary (#74836).

Move the decision into a pure resolveStagedUpdaterBinary() helper in
updater-process.ts and return null off Windows. The installer self-copies into
HERMES_HOME on every platform (paths::installer_dest,
bootstrap::copy_self_to_hermes_home), so finding that binary on macOS or Linux
is expected rather than leftover junk: declining to hand it an update is a
policy decision, and the comments now say so instead of describing the binary
as Windows-specific.

Cover the resolver in updater-process.test.ts: Windows accepts a staged
hermes-setup.exe, macOS/Linux return null even when hermes-setup exists, and
Windows returns null when nothing is staged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 22:33:53 -07:00
chenbin d649973751 fix(desktop): skip hermes-setup binary on macOS/Linux
resolveUpdaterBinary() returned a path on macOS if hermes-setup existed
in HERMES_HOME, routing macOS into the Windows-style quit→hand-off→rebuild
update dance. A stale hermes-setup (e.g. from 2026-06-08, predating the
applyUpdatesPosixInApp path) permanently breaks the in-app Update button.

The Tauri hermes-setup binary is a Windows-specific mechanism; macOS and
Linux use applyUpdatesPosixInApp instead. Always return null on non-Windows
so the native drag-and-drop updater is used on those platforms.

Fixes #74836
2026-07-31 22:33:53 -07:00
Teknium 484e451cb5 fix(tui): rebind extracted config handler to shared indicator constants
The salvaged commit renamed server.py's private _INDICATOR_STYLES/_INDICATOR_DEFAULT
to the shared hermes_constants imports, but methods_config.py (extracted after the
original PR was authored) still referenced the old private names through the
server-globals rebinding. Import the shared constants directly.
2026-07-31 22:33:16 -07:00
dongjiang cc0146f2de fix(cli): dispatch /indicator to set the busy-indicator style (#50618)
The /indicator command was registered in COMMAND_REGISTRY, listed in
/help, offered by tab-completion, recommended by the tips system, and
even documented in config.py — but it had no actual handler. Running
/indicator in the CLI produced "Unknown command: indicator".

Add _handle_indicator_command to CLICommandsMixin that:
- Shows the current indicator style when called with no args or "status"
- Validates the requested style against the shared INDICATOR_STYLES
  allowlist (ascii | emoji | kaomoji | unicode)
- Persists the choice to display.tui_status_indicator in config.yaml
  via the existing save_config_value helper
- Falls back to session-only when config save fails

The indicator-style allowlist is defined once in hermes_constants as
INDICATOR_STYLES + DEFAULT_INDICATOR_STYLE and imported by all three
consumers (CLI handler, command registry, TUI gateway config handler),
preventing drift between the TUI and CLI validation.

Also adds tests/cli/test_indicator_command.py covering dispatch,
validation, persistence, and registry integration.

Signed-off-by: dongjiang <dongjiang1989@126.com>
2026-07-31 22:33:16 -07:00
Teknium dd15a28250 chore: map contributor dongjiang1989 2026-07-31 22:33:11 -07:00
dongjiang de6a672168 fix(skills-hub): include owner in ClawHub source URLs and add retry on 429 (#51236)
Two fixes for the Skills Hub "View source" links on ClawHub skills:

1. Source URL generation was missing the required {owner} segment —
   https://clawhub.ai/skills/{slug} → 404. Correct format is
   https://clawhub.ai/{owner}/skills/{slug}. When the owner handle is
   unavailable, source_url is now "" (card omits the button) instead of
   emitting a broken link.

2. _fetch_owner_handle() previously delegated to _get_json() which
   returned None on any non-200 response with no retry. Under HTTP 429
   rate-limiting the "50 consecutive failures" safety rail in
   enrich_owners() fired immediately — the documented claim "Respects
   HTTP 429 rate-limit responses with exponential backoff" was not
   actually implemented. Now has its own retry loop: 3 attempts, honours
   Retry-After on 429, exponential backoff on 5xx/transport errors, no
   retry on 4xx.

Changes:
- tools/skills_hub.py: _coerce_skill_payload carries owner from top-level
  response; inspect() captures owner from detail API; _fetch_owner_handle()
  added with bounded retry/backoff; enrich_owners() batch method with
  safety rails (30 workers, early termination at 50 consecutive failures).
- website/scripts/extract-skills.py: _source_url() reads extra["owner"]
  for ClawHub.
- scripts/build_skills_index.py: batch enrichment step after crawling.
- tests: 35 URL/enrichment tests + 7 retry tests (42 total).

Signed-off-by: dongjiang <dongjiang1989@126.com>
2026-07-31 22:33:11 -07:00
Teknium 67d4800d67 docs(config): document --force destructive-replace semantics 2026-07-31 22:33:08 -07:00
Baophan00 a017297cf1 fix(config): respect --force for bare model key overwrite
--force was silently ignored for 'model' keys — the guard always
redirected to model.default even when the user explicitly asked to
replace the entire section. Now --force triggers a warning and
proceeds with the destructive overwrite for model too, matching
the non-model mapping --force behaviour.
2026-07-31 22:33:08 -07:00
Baophan00 99cfa8f063 fix(config): guard against scalar overwrite of mapping sections (#74995)
Prevent 'hermes config set <section> <scalar>' from silently destroying
an existing mapping. The bare 'model' shorthand is preserved by
redirecting to 'model.default' — all other mapping sections are refused
with a helpful error unless --force is used.

Closes #74995
2026-07-31 22:33:08 -07:00
Teknium 11f4c213a5 chore: map contributor Zeraphim 2026-07-31 22:33:03 -07:00
Zeraphim e050ca97ff test(background-review): reanchor unresolved-failure coverage 2026-07-31 22:33:03 -07:00
Zeraphim 2ace68ad37 fix(background-review): reject unresolved failures as skills 2026-07-31 22:33:03 -07:00
Teknium 3b1dfca207 chore: map contributor spfcraze 2026-07-31 22:33:00 -07:00
spfcraze b004041498 fix(cron): close GitHub auth-header exemption abuse in prompt scanner
Two holes in _strip_cron_safe_constructs (one a regression from
70411a615, two days old):

1. The [^\n]* tail erased everything after api.github.com on the line,
   so a payload smuggled after ; && or | was never scanned. A cron
   prompt carrying a benign-looking GitHub curl followed by
   'cat ~/.hermes/.env' or 'rm -rf /' passed the scanner and persisted
   (verified end-to-end through the cronjob tool). Bound the tail to
   the URL path ([^\s;&|]*), so same-line payloads survive the strip.
2. The (?:/|\b) host boundary treated lookalike authorities
   (api.github.com.evil.com, api.github.com@evil.com) as the trusted
   GitHub construct, erasing even exfil of the GitHub token itself to a
   non-GitHub host. Require the exact host followed by /, whitespace,
   or end.

Also add SSH private-key files to the read_secrets pattern — a
coverage gap found during adversarial testing (cat ~/.ssh/id_rsa was
invisible to the scanner even outside the exemption).
2026-07-31 22:33:00 -07:00
Xipong 0af8fb05bf fix(delegation): prevent child HERMES_SESSION_ID leak into parent process env
AIAgent.__init__ calls set_current_session_id(self.session_id), which
mutated both the task-local ContextVar and the process-global os.environ.
Because _build_child_agent wraps construction in delegated_child_context(),
the ContextVar write is harmless (task-local), but the os.environ write
clobbered the parent's HERMES_SESSION_ID for the rest of the process —
leaking the child id into parent tools and subprocesses spawned after
the child was built.

Root cause of HermesPRDelegationSessionContext: parent
20260729_212118_5d797e dispatched child 20260730_160515_736ea1; later
parent terminal inherited HERMES_SESSION_ID=the child.

Fix: set_current_session_id() skips the process-global os.environ write
when called from within a delegated_child_context(). The child's own
tools and subprocesses still resolve their id through the ContextVar
(task-local), while the parent's process-wide env keeps the parent's
session identity. Root agents (CLI, gateway, cron) retain both paths.

Adds 7 regression tests covering single child, concurrent children (8
parallel), parent-tool observation after construction, and root-agent
session rotation backward compatibility. All pass; ruff clean.
2026-07-31 22:32:55 -07:00
Xipong abd10ba7c9 test(agent): cover malformed steer budget path 2026-07-31 22:32:52 -07:00
Xipong 0fd0db1a8c fix(agent): preserve /steer through turn-budget enforcement 2026-07-31 22:32:52 -07:00
Teknium ae8131ba1b chore: map contributor praneshnikhar 2026-07-31 22:32:48 -07:00
praneshnikhar 35b44e0d5f fix(auth): apply same source-path write-through fix to non-pool xAI path
_save_xai_oauth_tokens had the identical self-sealing bug as
_sync_device_code_entry_to_auth_store: key-presence check before
_store_provider_state, which unconditionally creates the key.
Use _load_provider_state_with_source to decide write-through from
the actual grant source, not key presence.

Also update regression test per review: use the real
_write_through_provider_state_to_global_root helper and assert
rotated token pair values in the root store after each refresh
instead of just counting mock calls.
2026-07-31 22:32:48 -07:00
praneshnikhar 4e6299af48 fix(credential_pool): use source-path-based write-through to root (#74339)
_sync_device_code_entry_to_auth_store used key-presence on the profile
store to decide whether to write-through rotated tokens to the global
root.  _store_provider_state unconditionally creates that key, so every
refresh after the first self-disabled the write-through — root kept a
revoked refresh token and every other profile died with
refresh_token_reused / invalid_grant.

Fix: use _load_provider_state_with_source to learn where the grant was
resolved from.  When the source is the global root, write back only to
root and skip _store_provider_state so the profile never accrues a
shadowing providers.<id> key that blocks future root fallback.

Add regression test verifying write-through fires on refresh 2+, not
just the first call.
2026-07-31 22:32:48 -07:00
Gille 29eac371d1 fix(context): persist NVIDIA DeepSeek endpoint limit 2026-07-31 22:31:22 -07:00
Teknium 2f2d90344c fix(copilot): add explicit UTF-8 encoding to JWT store read/write
Matches the repo-wide convention (c89481db5e); unblocks ruff enforcement
and the Windows footgun checker on the salvaged JWT persistence code.
2026-07-31 22:31:09 -07:00
Teknium 1737741730 fix(copilot): follow-ups for salvaged PR #58743
- Bound ALL reads of the on-disk JWT store through one _read_jwt_store()
  helper (load, eviction, save-merge) — the 1 MiB cap previously only
  covered the load path; eviction and save could still parse an
  oversized/corrupt store and rewrite it back out (sweeper finding).
- Fix the class, not the site: the recovery gates checked the literal
  provider == "copilot" while /model and profile configs can leave the
  alias spelling in place (the reporter's own log shows provider=copilot
  AND provider=github-copilot in one session — the aliased turns would
  have silently skipped recovery). Single owner:
  AIAgent._is_copilot_provider() (slug aliases + Copilot base-URL
  fallback), used by both run_agent recovery methods and both
  conversation_loop gates.
- Update the salvaged 401 test to current main's client-retirement
  contract (release deferred to GC — no synchronous .close()).
- Add copilot_stale_cred_retry_attempted to the TurnRetryState field
  contract test; add bounded-store and alias-gate regression tests.
2026-07-31 22:31:09 -07:00
dstkwll 7779409a76 fix(copilot): recover from stale/degraded token 400 AND expired IDE-token 401
Copilot degrades in two related ways that both abort a turn as non-retryable
and only clear on a gateway restart (a cold process re-runs the token exchange):

1. HTTP 400 model_not_available_for_integrator / model_not_supported — a
   raw/degraded token routes to the restricted copilot-language-server
   integrator whose allowlist omits enterprise-only models (e.g.
   claude-opus-4.8). Because it is a 400 (not 401), the existing 401 refresh
   path never fired. Prevented (retry-with-backoff exchange + on-disk JWT
   persistence + header guard at the client chokepoint) and self-healed at
   runtime (single-shot forced re-exchange + client rebuild + retry before
   fallback).

2. HTTP 401 'IDE token expired: unauthorized: token expired' — the short-TTL
   *exchanged* IDE token expires mid-turn. The clean-401 path DID fire and call
   _try_refresh_copilot_client_credentials(), but that method only re-resolved
   the stable raw ghu_ token and rebuilt the client — it never evicted the
   cached exchanged JWT or forced a fresh exchange, so the retry put the SAME
   expired token back on the wire, 401'd again, and the single-shot guard
   aborted the turn. Fix: force a fresh IDE-token exchange (evict cached JWT via
   evict_cached_exchanged_token + re-mint via get_copilot_api_token) before the
   client rebuild, mirroring the merged auxiliary-path recovery (#59837) and the
   400 recovery in this same PR. Graceful fallback to the resolved token if the
   exchange endpoint is unreachable; picks up the enterprise base_url on
   re-exchange.

Brings main-loop clean-401 recovery to parity with the merged auxiliary path
(#59837), using the newer on-disk-aware evict helper. Companion context: #58743
(this PR, expanded), #51313, #63204 (which assumed the 401 path already
recovered — it reached the method but the method was too weak).

Tests: exchange retry/persist round-trip, restart-blip disk reuse, stale-cred
400 classifier, 400 recovery, and 3 new 401 cases (fresh exchanged token on the
wire; network-blip fallback to resolved token). 58 copilot tests green on
current main.
2026-07-31 22:31:09 -07:00
Teknium df8e42877b Merge pull request #75852 from NousResearch/fix/moa-completed-response-streaming-cluster
fix(moa): stream completed aggregator responses across all layers (#74031 + #74903 + #74543)
2026-07-31 22:30:47 -07:00
AKAZIK-py fdfddb55cb fix(moa): carry completed responses through the managed Relay path
Under managed Relay execution the provider factory runs lazily inside
provider_stream() on the Relay session's event loop. The MoA facade's
auxiliary call_llm(stream=True) is invoked from that callback, so the
eager final_response check added for the non-managed path never fires:
the inner ManagedLlmStream is returned to the outer stream, which then
synchronously iterates it on the same loop thread and dies with
RuntimeError: Cannot run the event loop while another loop is running
(the completed response effectively trapped one level deeper).

stream_current() now detects a running event loop and returns the raw
factory result instead of nesting a ManagedLlmStream: the outer managed
stream already provides Relay tracking for the enclosing attempt, and
its own completed_response_predicate traps the completed response as
final_response — the same contract the main streaming worker consumes
(chat_completion_helpers reads stream.final_response after the chunk
loop). Nested managed streams remain supported for genuinely streaming
providers via the outer stream's own iteration.

Adds managed-execution regressions using the retained relay_turn
fixture: direct completed-response trapping, and the nested
facade-shaped stream_current call.
2026-07-31 22:30:33 -07:00
AKAZIK-py 8fde4ff9d8 test(moa): cover call_llm(stream=True) completed-response unwrap at the outermost seam 2026-07-31 22:30:33 -07:00
AKAZIK-py ada3663ce7 fix(moa): unwrap completed responses in the auxiliary streaming path 2026-07-31 22:30:33 -07:00
Teknium ed77a34730 fix(update): make the ancestor-PID lock handoff cross-platform via psutil
The stale-staged-updater deadlock is not Windows-specific: hermes-setup
under ~/.hermes is only refreshed by a full installer run
(copy_self_to_hermes_home no-ops during --update), so every desktop whose
staged updater predates the HERMES_UPDATE_HANDOFF_PID export (8c76fe19)
runs an old parent that never sends the env var against a new child that
demands it — exit 2 ('Hermes is still running') forever, on macOS and
Linux just as on Windows.

Replace the wmic ancestry walk (deprecated, absent on current Win11,
GBK decode juggling) with psutil.Process().parents() — psutil is already
a hard dependency and is the project's canonical no-kill process probe.
Drop the os.name == 'nt' gate so all platforms heal. Add tests: a marker
owned by our parent process is recognized as our orchestrator; a live
non-ancestor holder is still refused.
2026-07-31 22:28:46 -07:00
xenodmc 6e8691f432 fix(update): fallback to ancestor-PID check for Windows update lock handoff
On Windows the Tauri updater writes its PID to the update marker, then
spawns 'hermes update' via the venv shim. The child receives
HERMES_UPDATE_HANDOFF_PID to recognize the parent lock, but on some
Windows configs the env var is not inherited through the shim subprocess
chain, deadlocking every GUI update.

Fix: when the env-var handoff fails, fall back to walking the Windows
process tree via wmic /format:csv to collect ancestor PIDs. If the
marker PID is found in the ancestor chain, treat it as our own
orchestrating parent and allow the update.

Also fixes wmic output encoding on localized Windows (GBK/CP936) by
using /format:csv which produces clean UTF-8 output.
2026-07-31 22:28:46 -07:00
teknium1 087b2230c4 fix(desktop): accept scheme-less host:port in the remote gateway URL field
Users pasting a Tailscale IP or LAN host as 'host:port' (no http://) hit
either a hard 'URL is not valid' error in the main process or, worse, a
silent dead probe in the renderer: the ^https?:// gates in the settings
and first-run forms never fired, so the field sat idle with no feedback.

- normalizeRemoteBaseUrl() (electron/connection-config.ts) now prepends
  http:// when the input has no scheme:// prefix; explicit non-http
  schemes (ws://, ftp://) still reach the protocol check and get a clear
  rejection.
- New renderer twin coerceRemoteUrlScheme() (src/lib/remote-url.ts),
  wired into both probe gates (gateway-settings.tsx and
  first-run-remote-form.tsx) so the debounced /api/status probe, sign-in,
  test, and save all see the coerced URL.
- Tests for both sides (electron/connection-config.test.ts,
  src/lib/remote-url.test.ts).
2026-07-31 22:28:43 -07:00
Teknium 859573d283 test: import LAZY_DEPS directly and make httplib2 pin a floor invariant
Replace the AST source-parse of tools/lazy_deps.py with a real import
(the module is importable; only setup.py legitimately needs AST since
it is a side-effectful standalone script), and convert the exact
httplib2==0.32.0 snapshot assertion into a >=0.32.0 floor so routine
future bumps don't break the test.
2026-07-31 22:28:21 -07:00
Yorkstone Supplies (sycamoregroupltd) 37e42808e7 fix(security): pin httplib2==0.32.0 in setup.py REQUIRED_PACKAGES (GHSA-j5g9-f88f-gfj3)
The previous fix (904ade32b) pinned httplib2==0.32.0 in pyproject.toml's
google extra and tools/lazy_deps.py's skill.google_workspace, but missed
a third install path: skills/productivity/google-workspace/scripts/setup.py
REQUIRED_PACKAGES. A user following the --install-deps path could still
resolve httplib2 via unpinned ranges.

This commit:
1. Exact-pins all four Google packages in REQUIRED_PACKAGES to match
   pyproject.toml and lazy_deps.py contracts exactly.
2. Adds a focused regression test that parses setup.py's REQUIRED_PACKAGES
   via AST and asserts every pin matches the other two install paths.

Changelog: fix(security), test(security)
2026-07-31 22:28:21 -07:00
Yorkstone Supplies (sycamoregroupltd) a7c26bbb5c fix(deps): pin patched httplib2 for google extra 2026-07-31 22:28:21 -07:00
Brooklyn Nicholson 69902c203e fix(desktop): toast when a send fails because the disk is full
Map ENOSPC / SQLITE_FULL / "disk full" error strings through notifyError to a
clear free-space toast, and fire it from rejected prompt.submit, gateway error
events, and terminal failure frames so a full disk never looks like silence.
2026-08-01 00:27:17 -05:00
Brooklyn Nicholson 24f346ee77 fix(gateway): fail prompt.submit when session storage hits a full disk
Disk-full / ENOSPC / SQLITE_FULL on first-message session persist used to be
swallowed as a debug log while prompt.submit still returned streaming, so the
send vanished with no error. Re-raise those failures, return a real RPC error,
and stamp session_persistence_failed turns with error so clients get a terminal
error frame.
2026-08-01 00:27:17 -05:00
brooklyn! 0454b370d1 Merge pull request #75862 from NousResearch/bb/cmdk-on-off
fix(desktop): put Toggle terminal on the ⌘K on/off pattern
2026-08-01 00:21:11 -05:00
brooklyn! e25aa3a338 Merge pull request #75873 from NousResearch/bb/logs-cmdk-only
Make the logs pane cmd+K-only — never auto-opened, never a standing tab
2026-08-01 00:18:59 -05:00
brooklyn! b0d1e803a9 Merge pull request #75869 from NousResearch/bb/terminal-theme-bg
Paint the integrated terminal with the transcript background
2026-08-01 00:16:09 -05:00