Commit Graph

953 Commits

Author SHA1 Message Date
jackoconner55 2f54ad4023 perf(cli): skip launcher-side plugin discovery for TUI handoff
(cherry picked from commit e2e0edd6b8ec10d02e867ff11dd1f9b4961a1ba7)
2026-08-15 00:36:03 -07:00
Adolanium ee1731b13c perf(cli): re-export decomposed command modules lazily, ~60ms off every CLI start
The main.py decomposition re-exported the sessions/update/dashboard command
surface with eager from-imports, so every hermes invocation (including
hermes --version) paid for update_cmd's dependency chain (jwt, click,
cryptography). Resolve the re-exports through the existing PEP 562 module
__getattr__ (same pattern as _PROVIDER_MODELS) so each module loads on
first actual use. Internal call sites go through a _self() helper because
bare-name lookups do not trigger __getattr__; _self() imports sys locally
since update tests patch hermes_cli.main.sys. The
_warn_stale_dashboard_processes back-compat alias moves into the lazy
surface, and the sessions argparse dispatch defers sessions_cmd to call
time. Monkeypatching hermes_cli.main.<name> keeps working: a patch sets a
real module attribute, which shadows __getattr__.

Measured (Windows 11, Python 3.11, median of 7 warm runs):
import hermes_cli.main 253ms -> 196ms (-22%).

(cherry picked from commit cad1083b71635f98815b698d69a18f7f58e15517)
2026-08-15 00:36:03 -07:00
Tachi d1df111ccd fix(update): restore Hermes Tools dependencies 2026-08-14 22:33:44 -07:00
Tachi 979a20052f fix(update): preserve activated extras across runtime rebuilds 2026-08-14 22:33:44 -07:00
Halldrix 19cff89300 fix(update): complete pending core install before any native import (self-lock loop fix)
Reviewer egilewski found the original defer was circular (#83590 comment):
the self-lock preflight wrote .update-incomplete and exited, but the next
launch only ran the full recovery AFTER main.py's third-party imports —
so a healthy venv's probes made the early pass a no-op, main.py imported
cryptography eagerly, the .pyd got mapped again, and the deferred install
re-hit the exact self-lock it was meant to escape.

Close the loop by making the marker guarantee the install runs BEFORE any
native extension can be imported:

- hermes_cli/_install_repair.py (new, stdlib-only): single source of truth
  for the core .[all] reinstall — ensurepip bootstrap, uv-pip/pip
  resolution with VIRTUAL_ENV, Termux env stripping, Windows hermes*.exe
  quarantine, per-extra fallback ladder, and fd1→fd2 routing for acp
  safety.  Deliberately free of managed_uv/hermes_constants imports so it
  stays importable in the corrupted-venv state it exists to repair.
- hermes_cli/_early_recovery.py: recover_if_needed now completes a pending
  .update-incomplete install BEFORE the import probes, on every launch
  that sees the marker (unless argv is update).  Success clears the
  marker; failure bumps an attempts counter inside the marker body and
  keeps it.  A 3-attempt ceiling stops a persistently-failing install
  from reinstall-hammering every launch (hermes acp included) — past the
  ceiling the late post-import recovery takes over with its manual
  recovery instructions.  Single-flight lock shared with the late path.
- hermes_cli/main.py: _recover_core_update_marker_locked delegates the
  install to the shared executor (no duplicated logic); ensure_uv stays
  in the late path so a venv whose uv vanished mid-update still
  bootstraps it.
- tests: 7 new regressions — the reviewer's exact case (marker + healthy
  venv → install runs while sys.modules has no cryptography), failure
  keeps marker + increments attempts, retry ceiling, lazy marker does
  not trigger core install (#58004 invariant), argv-update skip, and
  corrupt/missing marker bodies.  The key test was sabotage-verified:
  removing the pre-import branch makes it fail with zero install calls,
  while a lone-lazy-marker test still passes; restoring the branch makes
  it pass again.

Refs #83569
2026-08-14 22:03:56 -07:00
Halldrix c6a71294b6 fix(update): detect updater self-lock on Windows + repair venvs whose base interpreter is uv-managed
Two gaps left every Windows git-checkout install unable to recover from
the exact failure state #83569 reports:

1. Self-lock detection. _detect_venv_python_processes() always excludes
   the calling process by design — a CLI hermes update IS the venv python.
   An updater that had already imported a native venv extension (the
   canonical one being cryptography.hazmat.bindings._rust, mapped while
   hermes_cli.main resolved external secret sources) passed every
   preflight and then died mid-sync with os error 5 when uv tried to
   rewrite the mapped .pyd, stranding the venv half-updated. A new
   preflight now refuses the sync before touching the checkout, writes
   the update-incomplete marker so the next fresh launch completes the
   install, and exits 2. Verified on a live Windows 11 host: after
   importing hermes_cli.main, tasklist /m _rust.pyd shows the .pyd mapped
   in the caller, and a peer process cannot open it read-write
   (Permission denied) — while a rename succeeds, matching how uv/pip
   actually fail (truncate+write, not rename).

2. Early-recovery install path. _early_recovery._run_repair_install used
   sys.executable -m pip unconditionally. Windows git checkouts install
   on a uv-managed base interpreter (python-build-standalone), whose
   EXTERNALLY-MANAGED marker makes plain pip abort with
   externally-managed-environment — the repair no-oped and the venv
   stayed broken. The repair now detects the PEP 668 marker, prefers
   uv pip install with VIRTUAL_ENV pointed at the project venv, and
   falls back to pip --break-system-packages when no uv binary exists.

Both fixes ship with subprocess/unit regressions (sabotage-verified):
the new tests fail on pre-fix code and pass with it. Complements #77517,
which keeps the updater from importing cryptography in the first place;
this PR is the defence-in-depth when any future path loads it anyway.

Fixes #83569
2026-08-14 22:03:56 -07:00
PRATHAMESH75 517151ee4a fix(install): fail closed when a stopped gateway leaves an empty survivor probe
Review follow-up (#78574): the aborted-restart handler only flagged the fleet
stale when the post-failure survivor probe was None or non-empty. A positive
empty probe was treated as proof-of-safety — but `[]` is only safe when
nothing was running before the phase. If a gateway was discovered, stopped
(SIGTERM/drain), and its replacement never came back, the probe is empty at
exactly that unsafe moment and the update reported success — the fail-open
contract this fix exists to close.

Snapshot the pre-restart gateway PIDs before any stop/drain and route the
handler decision through a pure _restart_phase_failure_is_incomplete() helper
that fails closed on an empty survivor set whenever a gateway existed
pre-restart (or the pre-state could not be read). Add decision-level regression
tests covering the stopped-without-replacement gap, unknown pre-state, and the
truly-no-gateway positive control.
2026-08-14 22:03:56 -07:00
PRATHAMESH75 95018b6bba fix(install): surface aborted gateway restart during hermes update
The gateway auto-restart phase in `hermes update` was wrapped in a blanket
`except Exception` that only logged at debug level. When the phase raised
early — e.g. importing `hermes_cli.gateway` from the freshly pulled checkout
inside a process that already loaded pre-update modules — every drain and
restart line vanished from the update output, the update printed
"Update complete!" and exited 0, and the still-running gateway kept serving
pre-update modules against replaced source files. The next Telegram turn died
with `ImportError: cannot import name 'is_trivial_prompt'`.

The handler now probes for surviving gateway processes and, unless it can
positively prove none are running, prints the cause plus a manual recovery
command and marks the fleet restart incomplete — which exits nonzero and
writes the gateway-mode exit-code marker, matching the existing
failed-or-stale-unit path.

Fixes #78574
2026-08-14 22:03:56 -07:00
kshitij b58fa89cd7 refactor: centralize dict-valued model.default coercion via shared helper
Promote _split_model_config_default to hermes_cli/config.py as the single
shared helper for flattening dict-valued model.default/model.model config.
All 8 defense-in-depth sites now route through it instead of inlining
their own isinstance checks with inconsistent key orders.

Changes:
- Add split_model_config_default() to hermes_cli/config.py (public)
- cli.py: _split_model_config_default delegates to shared helper
- Fix key extraction order: agent_runtime_helpers.py was reversed
  (default->model); now consistent (model->default) across all sites
- Remove provider-as-model-name fallback from main.py, oneshot.py,
  model_tools.py, cli.py — provider is a routing key, not a model ID
- Add 'name' to _normalize_root_model_keys flattening loop and
  _has_nested_default detection to cover the deprecated model.name alias

Tests: 118 passed + 1 skipped (cli_init, managed_scope, config).
E2E: 31/31 passed (config chokepoint, managed scope, crash site,
negative cases, edge cases).
2026-08-15 10:29:05 +05:30
Ario Bagus Prakusa cb4daf23f3 fix: coerce dict-valued model/default config back to string across resolution paths
A dict-valued model.default (e.g. {provider:..., model:...}) in config.yaml
was leaking into agent.model and crashing the agent at init:

  AttributeError: 'dict' object has no attribute 'lower'
    agent/agent_runtime_helpers.py: anthropic_prompt_cache_policy

This manifested on the Telegram gateway as an infinite reset loop: every
turn built an agent with model=dict, crashed during init, the gateway
treated the failed turn as a session needing reset, and /reset rebuilt the
agent and crashed again.

Coerce dict -> string at every model-resolution entry point so the value
is normalized once and never reaches a .lower() call as a dict:
- agent/agent_runtime_helpers.py: anthropic_prompt_cache_policy (the crash site)
- agent/agent_init.py: configured default model resolution
- cli.py: CLI config model + _normalize_model_for_provider
- hermes_cli/main.py: _has_any_provider_configured
- hermes_cli/oneshot.py: _run_agent model resolution
- hermes_cli/runtime_provider.py: _get_model_config default handling
- model_tools.py: _resolve_active_context_length
2026-08-15 10:29:05 +05:30
Jeremy 1f6f86119f fix(cli): stop hermes update from respawning orphan serve --port 0 (#78821)
Filter manual dashboard/serve respawn candidates after update: skip
ephemeral --port 0 backends (Desktop-owned), dedupe normalized cmdlines,
and cap one restart per profile/HERMES_HOME so orphan counts no longer
grow across successive updates.
2026-08-14 21:46:32 -07:00
joaomarcos 39e480c051 fix(state): close leaked SessionDB connections on exception paths (#83226)
SessionDB could leave native SQLite handles open when construction failed
partway through schema/pragma/FTS/repair/lock/interrupt handling. Other
short-lived callers (MCP reads/polling, session search, reactions, trace
upload, insights, shutdown recovery) opened temporary SessionDB handles
without a complete ownership boundary. API-server profile caches and
RetainDB shutdown had similar late-close races. Under sustained load this
exhausted file descriptors (EMFILE).

- Close partially initialized SessionDB connections on every constructor
  exception path via a finally block guarded by an initialization-complete
  flag.
- Close temporary/cross-profile SessionDB handles in finally blocks across
  CLI, MCP, search, trace, reactions, insights, and recovery paths.
- Add API-server per-profile cache ownership and disconnect cleanup.
- Make RetainDB writer-queue shutdown exception-safe: track connections per
  thread, close on worker exit, reject new enqueues after shutdown starts,
  and sweep any connections left by short-lived threads.
- Add regression coverage for constructor failures, worker-thread readers,
  API disconnect failures, shutdown recovery, RetainDB late enqueue, and
  foreign-loop async clients.

Salvage notes: the original PR's per-thread WAL-reader ownership changes
were superseded by main's read-connection pool (permits + checkout/return);
its cron timeout-abandon fix is credited separately to #72822's earlier
identical fix.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-14 21:41:26 -07:00
gkd2323c 294071bf08 fix(banner): stop fabricating '1 commit behind' on SSH-official remotes
The SSH-official-remote path in _check_via_local_git was hard-coded to
return 1 when _check_via_rev reported UPDATE_AVAILABLE_NO_COUNT, so
'hermes --version' and the CLI banner surfaced a stable but false
'Update available: 1 commit behind — run hermes update' message. The
count never grew: whether upstream was 1 commit or 100 commits ahead,
the banner always said '1 commit behind'.

Root cause: an ls-remote probe against the upstream URL can only tell
us tip SHAs, not a real commit count. Returning the sentinel
UPDATE_AVAILABLE_NO_COUNT (-1) already means 'update exists, count
unknown' — the exact right shape for this path.

The dashboard/desktop UI does not depend on the fabricated 1:

- The REST /api/hermes/update/check endpoint
  (hermes_cli/web_server.py::check_hermes_update) treats any nonzero
  behind as update_available=true, and its docstring explicitly
  documents -1 as a legitimate value.
- The desktop store (apps/desktop/src/store/updates.ts::mapBackendCheck)
  clamps behind<=0 to 0 and reads updateAvailable as a separate boolean
  field.

So restoring the sentinel is a strict improvement: CLI banner and
hermes --version now say 'Update available' honestly instead of
inventing a count, and every REST/desktop consumer keeps working.

Changes:
- hermes_cli/banner.py: drop the 'return 1' override in the SSH branch;
  propagate the sentinel unchanged.
- hermes_cli/main.py::_print_version_info: render the sentinel as
  'Update available — run <cmd>' (without a count).
- tests/hermes_cli/test_update_check.py: update the SSH-official test
  to assert on the sentinel; add 3 new tests covering the CLI
  renderer's -1 / >0 / 0 branches.
2026-08-14 13:09:44 -07:00
Teknium 6f53373eb2 fix(cli): route startup guard through the selection-guard registry; cover the light oneshot fast-path
Follow-ups on top of the salvaged #70324:
- _confirm_startup_expensive_model_override evaluates the unified
  registry (combined_selection_warning) so id-keyed guards like the
  data-training-tier warning fire at startup too, not just the cost guard.
- The Termux-adjacent light oneshot fast-path (added after the PR
  branched) ran _run_and_exit_oneshot without the guard — same bug
  class, third sibling site now covered.
2026-08-14 01:31:31 -07:00
lkz-de 83d373aae6 fix(cli): guard expensive startup model overrides
Run the expensive-model warning for explicit startup `-m` / `--provider`
overrides before the chat loop starts, and fail closed for non-interactive
invocations that select an expensive or known-confusing model.

Also classify Nous paid-model 404s that say credits are required as billing
exhaustion so they fail fast with billing guidance.

Tested:
- scripts/run_tests.sh tests/hermes_cli/test_cli_startup_model_cost_guard.py tests/hermes_cli/test_model_cost_guard.py tests/agent/test_error_classifier.py -- --tb=short -q
2026-08-14 01:31:31 -07:00
brooklyn! 529ee80ac0 Recover from a half-replaced desktop bundle instead of requiring a reinstall (#85887)
* fix(desktop): load the intact renderer bundle when an update tears one copy

index.html and the hashed chunks it names are one generation. A packaged app
ships that bundle twice (inside app.asar and, via asarUnpack, beside it in
app.asar.unpacked), so an update that replaces the app while its files are
locked can leave the two copies from different generations. resolveRendererIndex
took the first index.html that existed, so it could pick the torn one and the
window died on its first lazy import with "Failed to fetch dynamically imported
module" -- with no way out, because every relaunch reloaded the same copy.

Check each candidate's declared modules and prefer a complete generation; when
both are torn, log which files are missing and how to repair instead of leaving
the crash unexplained.

* fix(cli): rebuild the desktop app when its renderer bundle is half-replaced

The content stamp hashes the SOURCE tree, which an interrupted update leaves
intact, so `hermes desktop` reported "up to date" and skipped the rebuild that
would repair a torn bundle -- the app relaunched into the same crash and
reinstalling looked like the only option.

Treat a bundle whose index.html names missing chunks as stale regardless of the
stamp, and say so on the way into the rebuild.
2026-08-14 02:21:20 -05:00
Teknium 5e8d25d7e7 fix(cli): --in accepts Git Bash / MSYS-style paths on Windows (#85865)
* fix(cli): --in accepts Git Bash / MSYS paths on Windows

Under Git Bash, 'hermes chat --in ~' reaches the CLI as /c/Users/<user>
(the shell expands ~ to an MSYS POSIX path; MSYS2 argument conversion
is disabled for native executables), and the isdir check failed with
'--in directory not found: /c/Users/...'. Route the value through the
existing _msys_to_windows_path translator (MSYS + Cygwin + WSL drive
spellings; no-op elsewhere) before expanduser/abspath.

Hit live: Bot Mode's agent-messaging protocol delivers with --in ~, so
every bot-to-bot send from a Git Bash-driven agent failed on Windows.

Tests pin both the translation cases and (source-level) the call site
actually using it.

* test: assert the MSYS translation, not platform abspath

The prior assertion ran os.path.abspath on the translated Windows path,
which on the Linux CI runner (posixpath) treats 'C:\Users\alice' as
relative and prepends the runner cwd. Pin the translation output and
ntpath absoluteness instead — same contract, platform-independent.
2026-08-13 23:41:49 -07:00
Teknium d254ad616f fix(cli): align _build_web_ui's npm closure with hermes update's (ui-tui + web + --include-workspace-root)
_update_node_dependencies() installs the unified closure, but update then
calls _build_web_ui(), whose 'npm ci --workspace web' pass deleted
node_modules and re-reified only the web closure — pruning root
devDependencies and the ui-tui hoisted deps the previous step just
installed, while exiting 0. Since the manifests digest was already
recorded, later no-op updates skipped the repair.

Reported by @andrexibiza in the #44772 final review (P1). Reproduced
E2E: '--workspace web' alone removes typescript-eslint/@eslint/js from
root node_modules; the unified closure restores them.

Guards: ui-tui only named when its manifest exists (prebuilt checkouts),
web-own-lockfile (#42973) and Termux (#38772) paths unchanged.
2026-08-13 02:38:28 -07:00
Teknium 2d91c085e3 Merge PR #41236 (Linux keychain auto-detect) onto current main 2026-08-12 17:07:45 -07:00
Eva acb7547dac fix(runtime): make nofile soft limit configurable 2026-08-10 17:02:56 -07:00
ethernet 37e46c774c cleanup: remove references to simple-term-menu
we migrated away long ago.
clean up all docs references the dependency itself
2026-08-10 15:13:29 -04:00
Teknium e5bc6b2186 fix(attribution): correct AI_AGENT id to registry value and carry harness markers into all terminal backends
The Hugging Face agent-harness registry matches standard-var values
EXACTLY against the harness id. Our registry id is 'hermes-agent'
(huggingface.js agent-harnesses.ts), so AI_AGENT=hermes was counted as
'unknown' — fixed at both entry points.

Remote terminal backends (Docker/SSH/Modal/Daytona/Singularity/Vercel)
never inherit the Hermes process env, and the cross-session leak guard
deliberately strips HERMES_SESSION_* from subprocess envs in engaged
multi-session hosts — so hf/huggingface_hub traffic from those shells was
unattributable. _wrap_command now exports AI_AGENT/HERMES_AGENT inside
every wrapped command with ${VAR:-default} semantics (outer harness is
never clobbered), and the snapshot dump excludes both names so a baked
value can never shadow a later outer harness.

E2E: verified against real huggingface_hub 1.27.0 detect_agent() with a
cached registry — 'hermes-agent' detected via AI_AGENT and via
HERMES_SESSION_ID; old 'hermes' value reproduced the 'unknown' bug.
2026-08-10 11:07:22 -07:00
Teknium e47a931d33 Port from earendil-works/pi#7493: advertise AI_AGENT env var for child-process attribution
CLI and gateway entry points now set AI_AGENT=hermes (the emerging
cross-agent standard read by e.g. huggingface_hub agent detection) and
HERMES_AGENT=true, via setdefault so an outer harness is never
clobbered.
2026-08-10 11:07:22 -07:00
Teknium 55f9e472a0 perf(cli): sub-400ms warm startup — probe-mode check_fns, lazy MCP SDK, banner snapshot, parallel worktree add
Cold CLI time-to-banner was ~1.8s (hermes) / ~2.8s (hermes -w). The banner
path was paying for work the session doesn't need before first input:

- aux availability probes built REAL OpenAI/httpx clients (openai import
  ~0.3s + SSL context) just to answer check_fns. New aux_probe_mode()
  returns a cache-excluded stub; resolution policy unchanged.
- tools/mcp_tool imported the mcp SDK (~260ms, mcp.types pydantic model
  construction) at module import even with zero MCP servers configured.
  SDK import is now lazy behind _ensure_mcp_sdk(); _MCP_AVAILABLE is a
  find_spec probe so every existing gate/test keeps its semantics.
- banner blocked 500ms on the update-check prefetch; now waits 50ms and
  defers the warning line to a daemon thread (prints above the prompt).
- banner recomputed get_tool_definitions + skills scan + git state every
  launch; now snapshotted to ~/.hermes/cache/banner_snapshot.json keyed on
  (config.yaml, .env, checkout rev, toolsets) and replayed on warm launches
  with a background refresh. Agent tool list is still computed fresh.
- _resolve_active_context_length probed the Nous portal /models (~200ms
  network) per launch; the tool-search gate now prefers the on-disk
  context cache when present.
- schema reconciliation re-executed SCHEMA_SQL in a scratch SQLite DB
  (~85ms) per SessionDB(); the reference parse is now disk-memoized by
  DDL hash (live-DB diffing still runs every startup).
- bundled-skills sync (~120-170ms rglob/hash) moved off the startup path
  to a daemon thread; plugin discovery starts in the background and every
  synchronous consumer joins via discover_plugins().
- hermes_cli.auth imported httpx eagerly (~30ms); now a lazy proxy that
  test monkeypatching still reaches (setattr forwards to the real module).
- fast chat launch: unambiguous 'hermes'/'hermes chat' invocations skip
  building all ~40 subcommand parsers (bails to full dispatch on anything
  else, incl. container mode).
- -w path: git worktree add runs with checkout.workers=8 (0.6s→0.2s) and
  overlaps HermesCLI construction; --skills preload runs in the background
  and is folded in at agent init (finalize_preloaded_skills, same
  fail-loud contract for fully-unknown skill lists); stale-worktree prune
  moved off the banner path.

Warm results (PTY time-to-banner, 5-run): hermes 1.80s → 0.38-0.40s;
hermes -w -s hermes-agent-dev --yolo 2.82s → 0.57-0.69s.
2026-08-10 10:40:19 -07:00
joaomarcos c790ed2a5d fix(state): recover gateway sessions stranded without a routing identity
When state.db's write path fails (corrupt FTS, or a crash landing between
routing publication and row creation), the live gateway conversation can end
up in a session row that never received its identity columns: session_key,
chat_id, chat_type and origin_json are all NULL. In-memory routing hides the
damage for as long as the gateway stays up. After a restart the chat is
resolved from the DB, and find_latest_gateway_session_for_peer cannot see
that row — both of its queries match on the very columns it lacks — so the
chat resumes the last keyed sibling instead, days older. The messages were
never lost, only unreachable.

Hardening the write side cannot reach a row that is already damaged, so add
the offline repair path the tracking issue asks for:

- SessionDB.find_orphaned_gateway_sessions() reports message-bearing rows
  with no session_key, and names the predecessor each one continues only
  when the evidence is unambiguous — a recorded parent_session_id
  ("lineage"), or exactly one keyed row of the same source and compatible
  user_id that fell quiet within 15 minutes of the orphan's start
  ("contiguity"). Contested pairs are reported with a reason and left alone:
  a wrong adoption would splice one person's conversation into another
  person's chat. Branch, delegate and tool rows are excluded — they are
  unkeyed by design, not by damage.
- SessionDB.adopt_orphaned_gateway_session() stamps the orphan from the
  predecessor (never overwriting a column that already has a value), records
  the lineage, and retires the predecessor under end_reason
  'superseded_by_repair' — a reason recovery does not treat as resumable, so
  the repaired row wins the chat from then on. The pair is re-verified inside
  the write transaction, making a concurrent heal a no-op rather than a
  conflicting write.
- `hermes sessions repair-routing` drives both. It reports without touching
  the database; --apply confirms first and warns that a running gateway
  still holds the old mapping in memory.

Refs #82616.
2026-08-09 14:06:06 -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
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
Teknium 9e6cfcda5a fix: finish the missing-encoding sweep — BOM-tolerant reads for user-edited stores
Complements the cherry-picked contributor fixes and closes out the
remaining sites of the 'missing explicit encoding' bug class, which is
now permanently gated by ruff PLW1514 (enabled repo-wide in
pyproject.toml and enforced by the blocking `ruff check .` step in
.github/workflows/lint.yml):

- tools/memory_tool.py: read MEMORY.md/USER.md via utf-8-sig so a
  Notepad BOM never glues U+FEFF onto the first entry (issue #10878,
  PR #10888 by @easyvibecoding — strict-decode contract of
  _read_raw_checked preserved rather than errors="replace", so
  undecodable files still refuse read-modify-write instead of being
  lossily rewritten). Regression tests included.
- tools/skills_tool.py: SKILL.md and skill file reads pinned to
  utf-8-sig + errors="replace" — deterministic across platforms instead
  of the locale fallback proposed in PR #51701 (superseded: falling back
  to cp1252/GBK makes the same skill render differently per host); .env
  reader aligned with the canonical utf-8-sig dialect in hermes_cli/config.py.
- agent/shell_hooks.py, hermes_cli/main.py, gateway/slash_commands.py:
  explicit utf-8 on the remaining fdopen/open text-mode sites flagged by
  the AlexFucuson9 sweep series (#56033 #56940 #65565 #66782 #66791).

Co-authored-by: easyvibecoding <easyvibecoding@users.noreply.github.com>
Co-authored-by: AlexFucuson9 <AlexFucuson9@users.noreply.github.com>
Co-authored-by: flyingdoubleg <wangzhe00zju@gmail.com>
Co-authored-by: LeonSGP43 <cine.dreamer.one@gmail.com>
2026-08-08 12:32:23 -07:00
Nolan 2fda6a384c fix(auth): cover remaining auth.json readers across modules
Follow-up to the auth.json UTF-8 read fix in this PR. A repo-wide scan for
the same bug class found three more callers that read ~/.hermes/auth.json
via Path.read_text() with no encoding — same Windows cp1252 hazard:

- agent/auxiliary_client.py _read_nous_auth: a non-ASCII byte raised
  UnicodeDecodeError, the broad except swallowed it, and Nous silently
  stopped being available as the auxiliary (vision/summarization) provider.
- tools/xai_http.py has_xai_credentials: same failure mode — xAI OAuth
  silently looked absent on Windows.
- hermes_cli/main.py is_setup_complete: same; has a config.yaml fallback so
  the impact is milder, but the read is still wrong.

All three now use read_text(encoding="utf-8-sig"), matching _save_auth_store's
write encoding. A repo-wide grep confirms there are no remaining
json.loads(...read_text()) reads of auth.json without an explicit encoding.

Tests: rewrote the Windows-encoding regression tests to actually exercise the
bug on POSIX too — a new windows_default_encoding fixture forces a no-encoding
read_text() to decode as cp1252 (the Windows default), and _write_utf8 now
emits real non-ASCII UTF-8 bytes (ensure_ascii=False) so the bytes actually
trip cp1252. Verified each test fails when its fix is reverted (including
the two new sibling-reader tests).
2026-08-08 12:32:23 -07:00
Teknium c5f5fa40c3 feat: --resume latest keyword and --in DIR launch flag
--resume latest resolves the most recent session through the same
workspace-scoped MRU lookup as -c (TUI source first under --tui, with
classic-CLI fallback). --in DIR chdirs before session resolution so the
lookup keys off DIR's workspace, and pins the session there by skipping
the recorded-cwd restore.

Requested by @Jeff9James: hermes --tui --resume latest --in ./dir
2026-08-08 04:03:41 -07:00
Teknium 47a35d63c0 Port from superagent-ai/grok-cli: verify subsystem (run-recipe detection + environment manifest + hermes verify smoke runner)
Scoped port of grok-cli's verify subsystem:
- agent/verify/recipes.py: static run-recipe detection mirroring grok's
  detection order (Node frameworks w/ lockfile-based package-manager
  choice, Django/FastAPI/Flask/generic Python, Go, Rust, Maven/Gradle,
  Makefile targets, docker-compose)
- agent/verify/environment.py: versioned, user-editable manifest at
  <project>/.hermes/environment.json; tolerant loader; manifest wins
  over fresh detection
- agent/verify/runner.py: bootstrap -> build -> test -> background start
  -> HTTP readiness poll -> process-group teardown, structured result
- hermes verify CLI command (--detect-only, --save, --skip-start,
  --phase, --port, --json)

Sources:
https://github.com/superagent-ai/grok-cli/blob/main/src/verify/recipes.ts
https://github.com/superagent-ai/grok-cli/blob/main/src/verify/environment.ts
2026-08-07 10:11:05 -07:00
Teknium 5db1b72b1f feat(cli): global emergency stop — hermes pause / hermes resume
Resumable ESTOP sentinel at $HERMES_HOME/ESTOP that halts NEW work only:

- agent/estop.py: sentinel engage/disengage/is_engaged (single stat, no
  caching), optional reason + timestamp stored as JSON, paused_reply()
  notice, check_paused() log-once-per-engagement helper. Corrupt/empty
  sentinel still pauses (fail safe); a `touch ~/.hermes/ESTOP` works.
- cron/scheduler.py: tick() skips dispatch while engaged (logged once per
  engagement, not per tick). Due jobs simply wait for the next tick after
  resume — in-flight runs are never touched.
- gateway/kanban_watchers.py: dispatcher skips auto-decompose and worker
  spawning while engaged; zombie reaping still runs and running workers
  finish naturally.
- gateway/run.py: new gateway turns (post-auth, non-internal) get a brief
  "Hermes is paused" reply instead of an agent run. Internal events
  (in-flight background completions) bypass the gate.
- hermes_cli/subcommands/pause.py: `hermes pause [--reason]` and
  `hermes resume`, wired into main() and _BUILTIN_SUBCOMMANDS.
- hermes_cli/status.py: `hermes status` shows a PAUSED banner (one stat).
- tests/test_estop.py: 20 tests — sentinel lifecycle, reason surfacing,
  log-once, cron skip + resume, kanban gate, gateway paused reply +
  internal bypass, CLI idempotence, builtin-set parity, status line.

Never kills in-flight work; resumable with no restart. Footprint ladder:
CLI command only, no new model tool, no new env vars.

Ported from: gastownhall/gastown estop.go (MIT); related prior art:
#26778 (/panic — kill/exit semantics, deliberately different: ours is a
resumable pause), #44617 (interrupt in-flight cron — out of scope here).
2026-08-07 08:58:14 -07:00
Brooklyn Nicholson 950b55d4d7 feat(update): emit an action-scoped terminal receipt from hermes update
The dashboard now mints an action_id per backend update, hands it to the
spawned `hermes update` via HERMES_ACTION_ID, and reuses an in-flight
update action instead of spawning a duplicate. The updater prints a
bounded `=== hermes-update completed <id> ===` receipt on every success
path — normal, zip, dependency-repair, and the no-op "Already up to
date!" path that previously ended with no terminal marker at all
(#58764) — so the Desktop can prove completion across the dashboard
restart boundary instead of guessing from stale log text.

Co-authored-by: Vitor Cepeda Lopes <vitor@vitorcepedalopes.com>
Co-authored-by: doncazper <caztronics@yahoo.com>
2026-08-05 10:34:18 -06:00
ethernet eea6044098 feat(desktop): register a Linux launcher entry for hermes desktop
On Linux a freshly-built desktop app had no presence in the application
launcher: no Hermes in the KDE/GNOME menu, no icon, nothing to pin. Users
had to hand-write ~/.local/share/applications/hermes.desktop and remember
to reindex the menu caches themselves.

`hermes desktop` now writes that entry itself (best-effort, idempotent,
never blocking a launch), and `hermes uninstall --gui` removes it again.

Both fields that matter are absolute:

- Exec — the launcher runs with a minimal environment and no shell PATH
  customizations, so a bare `hermes desktop` silently fails for anyone
  whose hermes lives in ~/.local/bin or a venv. We resolve the real binary
  via relaunch.resolve_hermes_bin(), falling back to an absolute
  interpreter + `-m hermes_cli.main`.
- Icon — an unqualified name only resolves against an indexed icon theme,
  which we are not in. The spec allows an absolute path, so we point at
  apps/desktop/assets/icon.png in the checkout. No copy is installed: Exec
  already depends on that same tree, so a second copy would add bytes and
  an uninstall step without surviving anything Exec wouldn't.

Menu-cache refresh is tool-gated — update-desktop-database, then
kbuildsycoca6 or kbuildsycoca5 — each only when the binary is actually on
PATH, because most desktops ship none of them and a missing one is not an
error. The entry is only rewritten when its contents change, so a launch
doesn't churn the caches every run.

Verified on NixOS: the generated entry passes desktop-file-validate, a
real kbuildsycoca6 on PATH is invoked with --noincremental, a real
update-desktop-database writes mimeinfo.cache, absent tools are skipped
cleanly, and removal leaves the checkout's icon untouched.
2026-08-05 11:47:05 -04:00
ethernet 84874c58a5 feat(dev-sandbox): support fake installer / fake main / git clones
allow you to simulate the whole official curl | bash installer,
and subsequent hermes updates.

Run development commands in a bubblewrap filesystem and network sandbox
with a local HTTPS MITM fixture server and a fake github
git-upload-pack transport.
Package the sandbox command and expose it from the nix devShell.

Stage the local installer at its canonical fake HTTPS URL and add a
persistent installation/update test path. Route root installs through
sandbox-owned filesystem locations and snapshot dirty source worktrees
into temporary fake commits so update tests can fast-forward without
changing the real checkout.

Includes a --install-ref sandbox installer mode that fetches any commit
(--from-main is a nice shorthand for local development) outside the
sealed sandbox, installs from that snapshot, and then promotes the fake
remote to the current worktree so update flows can be exercised with FF.

Notes on non-root sandboxes:
Giving a non-root sandbox a network is tricky.
slirp4netns joins the target userns and setuids to root before configuring the
netns, so the userns must map a uid 0; bwrap's --unshare-user maps exactly ONE
uid, so --uid 1000 leaves no root to become and slirp diedswith
`setns(CLONE_NEWNET): Operation not permitted`. Stage 1 builds the user+net
namespaces with `unshare` and two one-id ranges:

    inner 0    -> a subuid, unused by the payload, present only so slirp can
                  become root
    inner 1000 -> our real host uid

Mapping the payload to the *host* uid (not a second subuid) keeps everything the
sandbox writes owned by us, so `rm -rf` on a persistent sandbox still needs no
privileges. Stage 2 execs bwrap WITHOUT --unshare-user -- it only adds mount/pid
-- sidestepping bwrap's refusal to accept --uid outside a userns it created.
Costs a /etc/subuid range for the invoking user (we error with the exact line to
add) and util-linux `unshare`; `--root` needs neither.
2026-08-04 17:36:26 -04:00
frohsinnllc dbafb59227 perf(cli): check local auth.json/config before slow provider registry sweep
_has_any_provider_configured() probed every api_key provider (gh subprocess
for copilot alone takes 5s; full sweep ~18s) before consulting auth.json and
config.yaml, which are instant local reads. Desktop setup.status calls
blocked past the UI's timeout, causing the connect/disconnect boot loop.
Reorder so cheap local checks run first. Same semantics, ~35x faster here.
2026-08-04 12:28:39 +05:30
joaomarcos e18c040c3d fix(cli): back up state.db before clean-markers writes by default
purge_stale_tool_call_markers ran a permanent, irreversible UPDATE with
no backup — inconsistent with repair_state_db_schema's backup-by-default
convention for destructive state.db operations elsewhere in this file.

Take a full snapshot via VACUUM INTO (safe against a live connection,
unlike the raw-copy _backup_db_file used for malformed-schema repair)
before the write, timestamped beside state.db. Skipped when dry_run or
when there's nothing to change. Add --no-backup to `hermes sessions
clean-markers`, mirroring `sessions repair`.

Verified end-to-end: the CLI run against a real temp state.db produces
the backup file before printing the cleared-row count.
2026-08-04 11:26:15 +05:30
joaomarcos e1a2739692 feat(cli): add sessions clean-markers to permanently purge stale tool-call markers (#78148)
The load-on-read repair (_strip_stale_tool_call_markers) fixes affected
sessions in memory on every resume, but never touches the DB — long-lived
sessions re-scan and re-repair the same rows on every load, and the
contaminated bytes stay in state.db (and any backup/cache snapshot of it)
indefinitely.

Add SessionDB.purge_stale_tool_call_markers(dry_run=False): a one-time,
idempotent UPDATE that permanently blanks the content column on affected
rows. Only content is touched — tool_calls and every other column are
left untouched, so provider tool_call/tool_result pairing survives.
dry_run reads through the no-lock read path and never writes.

Wire it up as `hermes sessions clean-markers [--dry-run]`, mirroring the
existing optimize/repair subcommands. Verified end-to-end against a real
temp state.db: dry-run reports the row without writing, the real run
clears it and preserves tool_calls, and a second run is a no-op.
2026-08-04 11:26:15 +05:30
Rod Boev bdcdde9ff6 perf(cli): add --prefer-offline to npm install during update (#39267)
Re-derivation of PR #39399 onto current main: pass --prefer-offline to
the web-UI workspace install (both silent and verbose arms of
_install_web_deps) and to the update-time Node dependency refresh in
_update_node_dependencies, so npm reuses its local cache instead of
re-fetching metadata. Test expectations updated to match, mirroring the
PR's own test-update commit.
2026-08-03 17:18:31 +05:30
Teknium 58e3dcf3d6 chore: round-2 review nits (re-review #9)
- tests/agent/test_session_activity.py asserts against
  ACTIVITY_DESCRIPTION_MAX instead of the literal 120.
- The session-stall WARNING log line names its config knob
  (agent.session_stall_timeout) so operators can find the setting.
- hermes_state.py: collapse the triple blank line near line 191.
- hermes_cli/status.py no longer imports the private
  hermes_cli.main._relative_time: the helper moved to a public home
  (hermes_cli.timefmt.relative_time); main._relative_time stays as a
  thin back-compat wrapper (sessions_cmd and external patchers keep
  working).
2026-08-02 16:16:36 -07:00
Teknium 3829e34e23 feat(hooks): outbound webhooks — push signed lifecycle events to external HTTP endpoints
The inverse of the inbound webhook platform: hooks.outbound in
config.yaml lists HTTP targets + the plugin-hook events they subscribe
to (on_session_end, subagent_stop, post_tool_call, ...). Each firing
POSTs a JSON payload (same top-level shape as shell hooks' stdin wire)
signed GitHub-style with HMAC-SHA256 (X-Hermes-Signature-256).

Rides the existing hook bus — notify-only callbacks registered on the
plugin manager at the same CLI/gateway/main entry points as shell
hooks. Delivery is fire-and-forget via a bounded queue + single daemon
worker thread, so a dead endpoint can never stall a tool call. Bounded
retries (5xx/conn errors once; 4xx never). secret_env preferred over
inline secret. HERMES_SAFE_MODE skips registration. hermes hooks list
shows outbound targets with signed/UNSIGNED status.

Zero new model tools, zero new subsystems.
2026-08-02 15:01:11 -07:00
kshitijk4poor e4257c171a refactor: single chokepoint for the pre-import version fast path
Architecture fix for the bug class behind the Termux --version NameError
(live on main since eb4040242): version-printing kept being reimplemented
as *_fast() copies at the top of hermes_cli/main.py, each duplicating
canonical logic (project-root resolution, container detection, profile
detection). The copies drift silently — eb4040242 edited the canonical
output and referenced the PROJECT_ROOT module constant inside the fast
function, which doesn't exist yet at the fast exit point.

- hermes_cli/_startup_fast.py: THE implementations, stdlib-only. main.py's
  *_fast() names become thin delegates (kept for test/back-compat), and
  PROJECT_ROOT itself derives from the same helper — the constant and the
  fast path can no longer disagree.
- Fast output now includes the .install_method stamp (one cheap file read)
  and a 'Run hermes version for update status' pointer, so globalizing the
  fast path doesn't silently drop slow-path info.
- Guard tests: (1) import-weight — subprocess-imports _startup_fast and
  fails if any heavy module (config/yaml/argparse/cli/run_agent/httpx)
  lands in sys.modules; (2) subprocess parity on+off Termux — the test
  that would have caught eb4040242 the day it landed; (3) install-method
  stamp surfacing.

hermes --version: ~3.8s cold / 0.2-0.4s warm -> 0.01-0.02s everywhere.
2026-08-02 21:13:12 +05:30
kshitijk4poor d3832a24bc fix: fast-version follow-ups — PROJECT_ROOT NameError + renamed output label
- _print_fast_version_info referenced the PROJECT_ROOT module constant,
  which is defined AFTER the ultrafast exit point. On current main this
  is a LIVE latent bug: the Termux fast path NameErrors on --version
  (eb4040242 changed the print to use PROJECT_ROOT without noticing the
  constant doesn't exist yet on that path). Compute the root locally.
- PR tests asserted the old 'Project:' label; main renamed it to
  'Install directory:' (eb4040242). Expectations updated.
2026-08-02 21:13:12 +05:30
Zeheng Huang 3d20f106ca perf(cli): fast-path global version startup
(cherry picked from commit f7005276381e4c1fa8332c37bfc0adc2e0ab9b19)
2026-08-02 21:13:12 +05:30
ethernet 25d0bcd424 fix(runtime): resolve Hermes-managed Node and uv before bare PATH
Hermes installs runtimes for itself — `uv` at `$HERMES_HOME/bin/uv`, Node
at `$HERMES_HOME/node` — and neither directory is on an arbitrary
process's PATH. Every `shutil.which("node"/"npm"/"npx"/"uv")` in Hermes's
own code therefore has two failure modes: the managed runtime is invisible,
so the caller reports "not installed" or degrades to a slower tier on a
machine that has exactly what it needed; and when a system copy also
exists, the one Hermes does not own wins.

Routed the Hermes-owned call sites through managed-aware resolvers:

- `agent/lsp/install.py`, `hermes_cli/dep_ensure.py`, `hermes_cli/main.py`
  (`_make_tui_argv`), `hermes_cli/tools_config.py` (`_run_post_setup`) now
  use `find_node_executable()`.
- `hermes_cli/tools_config.py::_pip_install` and `hermes_cli/setup.py`'s
  vercel install use `ensure_uv()` (installing uv is in scope during setup,
  and the Windows installer's `uv venv` does not seed pip, so the fallback
  tier is "No module named pip"). `tools/lazy_deps.py` uses `resolve_uv()`
  — a lookup, not a bootstrap, because it runs mid-turn for an optional
  dependency and downloading a runtime as a side effect exceeds what the
  caller asked for.
- `hermes_cli/gateway.py`: extracted `_append_node_dir_for_service()`,
  shared by the systemd unit and launchd plist generators, which appends
  the managed dirs before the PATH-resolved one. A service definition is
  written once and survives reboots, so resolving a system Node that
  happens to lead the installing shell's PATH bakes the wrong interpreter
  in permanently. Managed dirs are profile-scoped, so each profile's unit
  still names its own Node; the existing symlink-parent rule (don't
  `.resolve()`) is preserved verbatim.
- `tools/environments/local.py`: the terminal tool's subshell PATH gains
  the managed dirs, appended alongside the sane entries rather than
  prepended — a tool the user deliberately put on their own PATH still
  wins, and the managed one only fills a gap. This is also what makes the
  bare `which("uv")` in `tools/env_probe.py` correct: that probe reports
  the environment the *model* sees, and the model can only run what is on
  that subshell's PATH.

`scripts/install.ps1`: the persisted User PATH update becomes
`Set-ManagedNodeFirstOnUserPath`, a move-to-front rather than an
add-if-missing. Installs made by an older install.ps1 already have the
managed dir in User PATH — at the tail, behind a system Node — and an
add-if-missing check sees it present and leaves that ordering in place
forever, so the users the bug hurt would never be repaired. Unrelated
entries keep their relative order (empty segments included; a trailing
`;` is legal and the installer's other PATH code preserves them),
duplicates collapse, and it writes only when the string actually changes.

Tests:

- `tests/test_managed_runtime_resolution.py` — AST guard that fails any
  new bare `which()` for a managed runtime, with a short justified
  allow-list and a companion test that fails when an allow-list entry goes
  stale. Reading source is banned by AGENTS.md and this is the documented
  exception: the property is "no call site anywhere spells it this way",
  which no runtime seam can observe.
- `scripts/ci/test_install_ps1_path_migration.ps1` — behavioral, not a
  source regex: it lifts the real `Set-ManagedNodeFirstOnUserPath` out of
  install.ps1's AST and rewrites only the two registry calls into an
  in-memory store, so the shipped split/dedupe/prepend/change-detection
  logic executes for real. Not in the default lane (Linux runners have no
  PowerShell host); runs under `pwsh`. 13/13 assertions pass.
2026-08-01 21:17:51 -04:00
Teknium 6b519255ea fix(update): provision a managed Node runtime when system npm fails engines.npm
The npm 12 requirement (f88ed6c717) strands every system-Node install:
no shipping Node bundles npm >=12, engine-strict makes EBADENGINE fatal,
and the recovery in npm_engine.py refuses to touch a system npm — so
'hermes update' leaves the install in a mixed state (updated code, stale
Node deps, no TUI/web/desktop rebuild) with only a manual-fix hint.

Instead of modifying the user's toolchain (still never done), the
EBADENGINE recovery now provisions Hermes' own managed Node tree under
$HERMES_HOME/node — the same pinned-nodejs.org path install.sh and
install.ps1 use — upgrades THAT npm into the required range, and hands
the caller the managed npm for its single retry.

- hermes_constants.bootstrap_hermes_managed_node(): cross-platform
  provisioning (POSIX via node-bootstrap.sh _nb_install_bundled_node,
  Windows via the existing portable-zip download); reuses a healthy tree.
- node-bootstrap.sh: HERMES_NODE_SKIP_LINKS=1 skips the ~/.local/bin
  node/npm/npx symlinks so the private tree never shadows the user's
  own toolchain on PATH.
- maybe_repair_npm_engine() now returns the npm path to retry with
  (managed-in-place upgrade or freshly provisioned runtime); both call
  sites retry with the returned path and put the managed tree first on
  PATH so npm lifecycle scripts resolve the managed node.
- Node-only mismatches on a foreign npm are now also recoverable (the
  managed tree ships a supported Node); on a managed npm they still
  correctly decline.

E2E (real download, temp HERMES_HOME): provisioned node v22.23.2,
upgraded bundled npm 10.9.4 -> 12.0.2, system npm byte-identical after,
no ~/.local/bin links re-pointed, healthy-tree reuse in 0.05s.
2026-08-01 16:40:50 -07:00
brooklyn! 97971643ab Merge pull request #76417 from NousResearch/bb/kanban-model-picker
Pick a kanban task's model and thinking depth from the board
2026-08-01 16:55:01 -05:00
Sora-bluesky 621975de85 fix(dashboard): anchor the SSH token dir to $HOME/.hermes, not the active profile
The Desktop client writes the SSH session token under $HOME/.hermes/desktop-ssh
(a literal ~/.hermes/desktop-ssh in apps/desktop/electron/remote-lifecycle.ts,
expanded against the account's $HOME), independent of HERMES_HOME and the active
profile. But _read_ssh_session_token_file validated it against
get_hermes_home()/desktop-ssh, which a non-default sticky profile re-homes to
<root>/profiles/<name>/desktop-ssh (and any custom HERMES_HOME points elsewhere).
relative_to() then rejects every token as "not under the desktop-ssh directory",
so SSH remote mode is broken under any non-default profile.

Anchor to Path.home()/.hermes/desktop-ssh so the validator matches the exact
directory the client writes to, across default, profile, and Docker layouts.
Adds profile / custom-root acceptance tests and a profile-local rejection test.

Fixes #69551.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-01 14:30:16 -07:00
Brooklyn Nicholson 0b69a6ac02 feat(kanban): let a task pin its own thinking depth
A task could already pin a model and provider, but not how hard the worker
thinks: reasoning effort came from the assigned profile's config and nothing
per-task could reach it. Pairing a small model with high effort, or a big one
with thinking off, meant editing the worker profile itself.

Adds a tasks.reasoning_effort column (migrated, NULL = inherit the profile)
with set_reasoning_effort(), a create_task kwarg, and a --reasoning spawn flag.
Kept deliberately independent of model_override: a task may run the profile's
own model at a different depth, and clearing a model override no longer resets
the depth the operator chose. "none" is a value (thinking off), not a clear.

--reasoning is new on the CLI too — the level was only reachable through the
/reasoning slash command, so the dispatcher had no flag to pass. It overrides
agent.reasoning_effort for one run and is never persisted.
2026-08-01 16:09:56 -05:00
kshitij 66ba36ec81 fix(update): let callers pass the platform verdict to the venv helpers
CI slice 8/8 red:

  test_verify_core_dependencies.py::test_uses_virtual_env_from_environment
  AssertionError: assert None == PosixPath('.../newvenv/Scripts/python.exe')

The Phase 2 reviewer flagged this exact risk (W4) and I under-weighted it as
"latent, not broken". It was neither — it was already failing.

The suite exercises Windows-only paths on Linux CI by patching predicates
(`hermes_cli.main._is_windows`, `is_windows`, `platform.system`). Routing
those call sites through a helper that reads `sys.platform` unconditionally
meant the patches no longer reached the path derivation: the test built
`Scripts/python.exe` while the code looked for `bin/python`.

venv_bin_dir/venv_python_path now take an optional `windows=` verdict,
defaulting to the host. Every converted site passes its own predicate, so
the patched-predicate coverage is restored — the dedup keeps the layout in
one place without hijacking the platform decision.

Verified by causation: dropping `windows=` reproduces the CI failure exactly;
restoring it goes green. Added two regression tests, including one asserting
a patched `_is_windows` still reaches the derivation.
2026-08-01 16:45:00 +05:30