Commit Graph

45455 Commits

Author SHA1 Message Date
kshitijk4poor
f660ba2289 fix(gateway): durable prompt pins never outlive their privacy policy
The persisted pin carried rendered session-context bytes but not the
privacy.redact_pii they were rendered under. After a restart, the first
internal event restored and reused them verbatim, so bytes rendered with
redaction off reached the model after redaction was switched on. Base
re-rendered under the current setting after a restart.

The pin now records redact_pii (the in-memory ephemeral_pin becomes
(change_key, text, redact_pii); the durable schema requires the flag).
Bytes from a different policy are never reused, including by internal
events in the same process, which had the same bypass until the next
human turn.

Simplified on the way:
- sanitize_prompt_pin is the one validator; the snapshot builder and
  rehydrate no longer re-check the shape by hand, and use
  PROMPT_PIN_VERSION instead of a literal.
- Rehydrate runs once, from the context path, and only when this process
  holds no pins for the key (a restart). Eviction clears only the context
  pin, so an evicted agent still re-renders instead of reviving the
  snapshot. The channel path no longer rehydrates or takes a session id.
- _persist_prompt_pins drops its synchronous get_prompt_pin pre-check,
  which took the store lock on the event loop; set_prompt_pin already
  no-ops an unchanged snapshot. Its call-count test goes with it.
- set_prompt_pin builds its candidate with dataclasses.replace like
  set_model_override, and no longer accepts None (no caller cleared).
- The read-fence test folds into the restart/reset test.
2026-09-28 15:54:07 +05:30
JoaoMarcos44
4bbbfce3a2 fix(gateway): omit malformed prompt pins from routing state
(cherry picked from commit 8928355d002697108443ac7b0edfcd28b02a2738)
2026-09-28 15:54:07 +05:30
teknium1
72a2271d0e test(gateway): accept preserve_prompt_pin in handoff switch_session stubs
The CLI-handoff commit passes preserve_prompt_pin=False to switch_session, so
the two hand-written stubs that fake the store on that path (secondary-profile
adapter, Telegram DM topic lane) raised TypeError on the new kwarg and the
exact-call assertion no longer matched. Widen the stubs and the assertion;
no production change.

(cherry picked from commit 679eacc84a044696421b7d0de358eb65126b88da)
2026-09-28 15:54:07 +05:30
JoaoMarcos44
3275dcead3 fix(gateway): fence prompt pin rehydration
(cherry picked from commit d15d25312dfaf62add09fe36d2203be5288d28fe)
2026-09-28 15:54:07 +05:30
JoaoMarcos44
bf7879bc17 fix(gateway): clear prompt pin on CLI handoff
(cherry picked from commit f38beed0a2de48947aae2086d998148fc409a7b7)
2026-09-28 15:54:07 +05:30
JoaoMarcos44
0ba9e1b7f0 fix(gateway): persist internal prompt pins
(cherry picked from commit 0da37221523633bef2c3454ece033f8eac285893)
2026-09-28 15:54:07 +05:30
kshitijk4poor
0d5aae5e24 fix(gateway): warn when config still declares a retired idle/daily session_reset
Time-triggered conversation rotation was removed in 1d5d059410 and core
has read nothing under session_reset since. Nothing told users whose
config still asked for it: their gateway conversations silently stopped
resetting, and a household that never types /new pays for an
ever-growing context.

Gateway startup (for every served profile) and `hermes doctor` now
report a session_reset block whose mode is idle, daily or both, and
point at the hermes-session-reset-policy catalog plugin, which reads
the same top-level block unchanged. The notice stays quiet while that
plugin is enabled. The config key is reported, never rewritten, since
the plugin consumes it. The gateway: form the old loader also accepted
is detected too, with a hint to move it to the top level.
2026-09-28 15:54:01 +05:30
teknium1
485979ddf4 fix(review): validate the logsRoot owner name; scope the whole palette and fail 4064 on an unknown profile
Independent-review findings on #126159:

- MAJOR: `hermes:fs:logsRoot` joined a renderer-supplied profile name into
  `profiles/<name>` unvalidated (session-owner data; from the remote backend in
  remote mode), so `..`/absolute values escaped the profiles root, were created
  and revealed. `localPluginsRoot` now accepts only `default` or a
  `DESKTOP_PROFILE_NAME_RE` match — the gate `localSkinProfileKey` and main.ts
  already apply — and otherwise falls back to the active Desktop profile.
- MINOR (b): `commands.catalog` scoped skill discovery to the routed profile but
  read quick_commands and plugin commands unscoped, listing the LAUNCH profile's
  quick commands in a profile-routed palette. All loaders run inside the one
  `_session_home_scope` block.
- MINOR (c): `commands.catalog` swallowed ProfileUnavailableError into a warning
  (ok + empty skills) while complete.slash returned 4064; `_profile_scoped_rpc`
  re-raises it so dispatch maps it to 4064 (also fixes skills.reload's 5025).
- MINOR (a): the shared `session_gateway_runtime` reader's new merge (top-level
  base_url/api_mode + billing_provider) gets a CLI-side test; the CLI-visible
  precedence change is disclosed in the PR body.
2026-09-28 03:13:19 -07:00
teknium1
9f8fb4a5b9 fix(desktop): error card's Open Logs reveals the session owner's logs dir (#119080)
Root cause: `hermes:fs:logsRoot` resolved `readActiveDesktopProfile()` — the
pooled backend's launch/active profile — so a failing turn in a chat owned by
a served secondary profile revealed `profiles/<launch>/logs` instead of the
`profiles/<owner>/logs` where that turn's agent.log/errors.log live.

The IPC now takes the owning profile; the error card passes its composer
scope's owner profile (falling back to the active gateway profile) and
Electron joins `<HERMES_HOME>/profiles/<owner>/logs` (`default` → the home's
own `logs/`). No caller → unchanged active-profile resolution.

Red-on-base: electron/fs-ipc.test.ts › hermes:fs:logsRoot › "resolves the
logs dir of the profile that owns the session" fails on origin/main with
Expected ".../profiles/finex/logs", Received ".../profiles/launch-profile/logs";
green on head (5 passed).

Profile-scope row: T7 (Desktop pooled serve, several homes per process) —
storage identity: lock/PID/state path under the OWNING home.
2026-09-28 03:13:19 -07:00
finn763
2b9fdb85d4 fix(desktop): API server card shows a served secondary as enabled (#121125, salvage #122165)
Root cause: in multiplex mode `_messaging_platform_payload` read enabled/
configured from the secondary profile's own never-configured config, so the
Desktop API server page printed Disabled / "needs setup" over the live
shared-listener mirror even though `/p/<profile>/v1` answered. A live
`mirrored_from` entry (only present while the shared gateway runs) now
projects as enabled + configured; own-gateway, stopped-gateway and default
profile paths are unchanged.

Red-on-base: tests/hermes_cli/test_served_profile_mirror_platforms.py::
test_messaging_card_shows_mirrored_api_server_enabled_without_local_config
fails on origin/main at `assert payload["enabled"] is True` (line 53);
green on head: 5 passed.

Profile-scope row: T7 (Desktop pooled serve, several homes per process).

Salvaged from #122165 by @Finn763 (cherry-pick, authorship preserved).
2026-09-28 03:13:19 -07:00
chelsealong
3fe934aabf fix(desktop): scope the new-chat slash palette to the rail-selected profile (#124651, salvage #124845)
Root cause: a draft has no session_id, so use-slash-completions sent commands.catalog /
complete.slash with empty params and tui_gateway's _session_home_scope (session-only
profile_home) scanned the LAUNCH profile's skills; CompleteSlashParams also rejected a
`profile` key outright (extra=forbid -> 4000).

Fix: the hook sends the tile's routed profile when there is no session (cache keyed by
session-or-profile); _session_home_scope resolves a session-less `profile` through
_profile_home; `profile` added to CompleteSlashParams and SkillsReloadParams (same
session_id-or-empty shape in skills.reload) and the TS contract regenerated.

Red-on-base: tests/tui_gateway/test_protocol.py::test_sessionless_slash_palette_follows_profile_param
fails on origin/main (complete.slash -> 4000 "profile: Extra inputs are not permitted");
use-slash-completions.test.tsx "scopes a sessionless catalog and typed lookup to the routed
profile" fails on origin/main (commands.catalog called with {} not { profile }). Both green on head.
2026-09-28 03:13:19 -07:00
teknium1
33c6ab002d fix(tui_gateway): resume on the route in model_config.gateway_runtime (#125942, salvage #125956)
tui_gateway/server.py::_stored_session_runtime_overrides read only the top-level model_config
keys and then fell back to billing_provider (frozen at the first accounted call), never the
nested gateway_runtime dict gateway/run_turn.py persists per turn — so a Telegram-driven session
resumed on Desktop paired the current model with the first-call provider and 404'd. Route the
Desktop/TUI reader through the canonical SessionDB.session_gateway_runtime (nested gateway_runtime,
then top-level TUI keys, then billing_provider), the same reader CLI --resume uses; the reader now
fills a missing top-level provider from billing_provider so the pre-existing TUI contract
(top-level base_url/api_mode + billing_provider) is unchanged. Redone slim from #125956, which
kept a second hand-rolled reader and inverted the precedence.

red-on-base: tests/tui_gateway/test_resume_switched_provider_endpoint.py::
test_resume_honors_gateway_runtime_over_first_call_billing_provider and
test_resume_prefers_gateway_runtime_over_stale_top_level_keys fail on origin/main with
`assert ('anthropic', None, None) == ('openai-codex', None, 'codex_responses')`; green-on-head.
2026-09-28 03:13:19 -07:00
Yuan Li
7fd2f57413 fix(cli): include -p <profile> in TUI exit summary resume hints (#125078, salvage #125079)
hermes_cli/main_tui_launch.py::_print_tui_exit_summary printed `hermes --tui --resume ID` /
`hermes --tui -c "TITLE"` without the `-p <profile>` suffix that the classic CLI sibling
(cli_session_mixin.py::_print_exit_summary) already appends, so a hint copied from a named
profile's TUI resolved against the default profile and failed with "No session found".

Trimmed from #125079: no try/except fallback (get_active_profile_name does not raise), tests
folded into the existing tests/hermes_cli/test_exit_summary_resume_hint.py.

red-on-base: tests/hermes_cli/test_exit_summary_resume_hint.py::TestTuiExitSummaryResumeHint::
test_tui_hints_include_profile_flag_for_named_profile fails on origin/main with
`assert 'hermes --tui --resume 20260524_000001_abc123 -p dev' in '...hermes --tui --resume
20260524_000001_abc123\n  hermes --tui -c "My TUI Session"...'`; green-on-head: 7 passed.
2026-09-28 03:13:19 -07:00
John Paul Soliva
46d0801279 fix(tui_gateway): /save writes the transcript under the session's own profile (#125241, salvage #125242)
Root cause: tui_gateway/methods_session.py session.save built saved_dir from get_hermes_home()
in an unscoped @_session_method handler, so a secondary-profile session's /save landed in the
launch profile's sessions/saved/. Use session["profile_home"] like the other session-bound
handlers (session.history / session.create); the compute-host path is unchanged. No other
handler in methods_session.py builds a session-bound path from get_hermes_home().

Red-on-base: tests/tui_gateway/test_tui_gateway_server.py::
test_session_save_lands_in_the_sessions_own_profile_a_b_a fails on origin/main at
`assert parents == [saved, work_home / "sessions" / "saved", saved]` (index 1 is the launch
home); green on head (3 session_save tests passed).
2026-09-28 03:13:19 -07:00
ahisblessed
332424ce45 fix(tui_gateway): bind the session's profile scope around slash side-effect mirrors (#122655, salvage #122698)
Root cause: tui_gateway/methods_slash.py::_mirror_slash_side_effects ran mirror(...) off-turn
(slash.exec RPC pool / compute-host control reader) with no profile runtime scope bound, so on a
multiplexed gateway /model's switch_model -> resolve_runtime credential read raised
UnscopedSecretError ("live session sync failed") and the live session kept its old route. The
wrap at this chokepoint covers every mirror (/model, /prompt, /personality, ...) and all callers.

Red-on-base: tests/tui_gateway/test_multi_profile_hosting_fail_closed.py::
test_model_switch_mirror_binds_runtime_scope_under_multiplex fails on origin/main at
`assert invoke(b) == ""` with "live session sync failed: Hermes could not read this profile's
A_ONLY_TOKEN ..."; green on head (1 passed).
2026-09-28 03:13:19 -07:00
Yuan Li
51b91997a5 fix(tui_gateway): dispatch agent_loop_stopped in the session's profile scope (#125063, salvage #125073)
Root cause: tui_gateway/session_lifecycle.py::_interrupt_session_turn invoked the
agent_loop_stopped hook with no profile runtime scope bound, so on a pooled serve /
multiplexed gateway an observer's get_hermes_home() named the launch profile instead
of the interrupted session's. Binding _session_profile_runtime_scope(session) inside
the function covers every caller (session.interrupt RPC, WS orphan reaper, idle reaper,
lease takeover, compute-host close); same rule _finalize_session already applies.

Trimmed from #125073: no hook-payload change, no plugins.py doc change, one A->B->A test.

Red-on-base: tests/tui_gateway/test_interrupt_agent_loop_stopped_hook.py::
test_interrupt_hook_observer_sees_the_sessions_profile_a_b_a fails on origin/main with
`assert seen == [launch, routed, launch]` (index 1 is the launch home); green on head (4 passed).

Co-authored-by: Yuan Li <dskwelmcy@163.com>
2026-09-28 03:13:19 -07:00
teknium1
01a5549faf test(e2e): shallow/partial-clone update cells assert the fixed behaviour
The full-clone shape (#122353), the second update after the unshallowing
update (#124272) and the --depth pre-fetch (#123346) cells run HEAD code and
now assert plainly. #123254 and #124645 live in N-1's pre-swap fetch and
check/pull code, so those gates stay until a release carrying the fix is N-1;
their reason text now says so.
2026-09-28 03:08:56 -07:00
teknium1
f338f4830c test(update): keep two invariant tests per fix in the salvaged unit tests
The upgrade E2E cells are the regression proof; the unit tests keep one
invariant per behaviour: full clone stays full / partial clone keeps its own
filter (#122353), crash recognizer + single promisor-disabled retry (#124272),
heal of a depth-1 clone / of a --depth-grafted full clone (#123254, #123346).

Co-authored-by: salch-cred <salch-cred@users.noreply.github.com>
2026-09-28 03:08:56 -07:00
teknium1
010618c767 fix(update): one commit-graph fetch for the pre-fetch heal and the post-update pass
The pre-fetch heal (#123254) and the post-update tag fetch now share
fetch_full_commit_graph, so both follow one clone-mode rule: a partial clone
repeats its own filter, a full clone fetches unfiltered (#122353), and only a
depth-limited clone whose history is really missing unshallows as tree:0. A
full clone grafted by a user's `git fetch --depth 1` already has its history
locally, so it unshallows unfiltered and stays full.

Also: the network-timeout message no longer claims "no response from the
remote" (a large transfer on a slow link hits the same cap), without adding a
classifier rule that would assert the opposite.
2026-09-28 03:08:56 -07:00
Konstantin Khlopkov
e377309d86 fix(update): heal stale shallow history before the bounded fetch
A depth-1 installer checkout far behind main could never update: the plain
main fetch pulled nearly the full object graph (merge-heavy history drags in
side-branch ancestry past the shallow boundary), hit the 300s network cap
mid-transfer every time, and the unshallow step that makes later fetches
incremental only ran after a successful update. Unshallow the checkout first
(commit-graph-only fetch with the same 900s budget the post-update tag fetch
uses, pulling the update branch along), non-fatally, and stop reporting a
wall-clock kill as 'no response from the remote'.

Fixes #123254
2026-09-28 03:08:56 -07:00
liuhao1024
6b51b0fd4e fix(update): expire fetch reflogs that pin stale shallow grafts
prune_stale_shallow_grafts dropped grafts no live ref points at, but the
remote-tracking reflog still naming a dropped fetch tip made the --reflog
fail-safe walk fail (its parent was never fetched), rolling the prune back
on every run — grafts kept accumulating and the next update fell into
orphan divergence (#124645). Expire exactly the fetch reflogs that pin a
dropped graft before writing the trimmed file; user reflogs (HEAD, local
branches) are never touched, so the fail-safe rollback still guards them.

Fixes #124645
2026-09-28 03:08:56 -07:00
Yuan Li
ffa8817345 fix(update): recognize the Windows pack-objects crash stderr in the partial-clone recovery
Git for Windows 2.54 reports the same partial-clone pack-objects BUG()
without the POSIX 'pack-objects died of signal 6' line (#124293 field
evidence): the BUG assertion is followed directly by 'fatal: could not
finish pack-objects to repack local links' and 'fatal: index-pack failed'.
The recognizer demanded the signal wording, so the recovery retry missed
this Windows occurrence entirely.

Require the BUG fingerprint plus EITHER terminator of the same abort —
the POSIX signal line or the Windows repack fatal — and pin both stderr
shapes plus a fingerprint-without-terminator negative in the regression.
2026-09-28 03:08:56 -07:00
Yuan Li
fc6144e3c1 fix(update): self-heal git pack-objects crash on partial-clone fetches
On a partial clone (clone --filter=tree:0), git 2.53/2.54 crashes every
fetch: index-pack --promisor runs repack_local_links(), which feeds
pack-objects --exclude-promisor-objects-best-effort, and pack-objects
BUG()s (SIGABRT, 'should only be called on existing objects') when the
link traversal hits a legitimately missing promisor object. The crash is
deterministic, so every fetch — and with it the CLI's update/check flows
— stays broken until the user applies the manual workaround themselves.

The recovery is the reporter's verified workaround, wired in: on this
exact crash (all three stderr markers), retry the fetch once with
-c remote.origin.promisor= (per-invocation only; the user's filter
choice stays in their config). Wired into both fetch sites — the update
fetch and the check fetch (which also serves forks via upstream).
Unrelated fetch failures are returned untouched; if the retry still
crashes, the update path prints the manual heal command.

Fixes #124272
2026-09-28 03:08:56 -07:00
Yuan Li
5849446b02 fix(update): repeat the clone's own partial-clone filter, not tree:0
Hardcoding tree:0 for partial clones would silently tighten a checkout the
user cloned with --filter=blob:none. Read the existing
remote.origin.partialclonefilter instead and pass it back verbatim; full
clones still fetch unfiltered.
2026-09-28 03:08:56 -07:00
Yuan Li
de98f88909 fix(update): do not re-arm partial-clone config on the post-update tag fetch
git fetch --filter=tree:0 writes remote.origin.promisor=true and
remote.origin.partialclonefilter=tree:0 even when those keys were
deliberately removed, so every hermes update converted a de-partialised
checkout back into a partial clone and re-armed the should_include_obj
fetch failure (#122353). Pass the filter only when the checkout already
is a partial clone; full clones fetch tags unfiltered.
2026-09-28 03:08:56 -07:00
teknium1
35272ce28b fix(state): /proc/locks readers no longer lose entries to other processes' lock churn
/proc/locks is host-wide and the kernel serves it over several read() calls,
resuming by record index. When other processes add or drop locks between
those reads the table shifts and entries are skipped, so a lock that is
still held can read as gone.

- tests: the live-DB preview and header-probe lock tests read their own
  process's locks from /proc/locks and flaked on busy CI hosts (1 in 12 on
  a quiet box; 9 in 12 with six processes churning locks). A shared
  tests/posix_lock_probe.own_posix_locks() reads /proc/self/fdinfo instead,
  which lists this process's locks per open file in one read.
- hermes_state_lockowners: state_db_write_lock_holders() names the writer
  in the "state.db write lock unavailable" warning. One pass could skip the
  holder and log "no write-class lock held at the deadline". It now unions
  three passes; a skip rarely repeats.
2026-09-28 02:26:21 -07:00
teknium1
b22631ec4f test(e2e): drop stale gates for #124643 (detached HEAD) and #123424 (duplicate ~/.local/bin)
Both issues are already resolved on main and both cells pass there ungated; the gates only hid a
future regression.
2026-09-28 02:00:41 -07:00
teknium1
a31e618e70 fix(pm): download npm-hosted tools through the user's npm registry
pm/lock.json and the Npm/AgentBrowser templates record registry.npmjs.org, and
pinned_source handed that URL straight to the downloader, so ~/.npmrc /
npm_config_registry were ignored and closed networks could not provision npm or
agent-browser. pinned_source now routes public-npm URLs through the configured
registry (npm's own precedence: npm_config_registry, then the user npmrc); the
lock's SHA256 still verifies the bytes and progress keeps the lockfile URL.
npm_dist_tags reads from the same registry.

The E2E cell test_npm_registry_mirror_serves_pm_npm_download drops its gate.

Co-authored-by: funky-xamarin <30426178+Wenfengcheng@users.noreply.github.com>
2026-09-28 02:00:41 -07:00
teknium1
86f20a367a test: trim salvaged SSL/config unit tests to two invariants each; flip the #124654 and #119928 E2E gates
The E2E cells (test_update_with_corporate_root_only_in_ssl_cert_file, test_p2_env_lock_refusal_is_not_reported_as_success)
now assert the fixed behaviour directly.
2026-09-28 02:00:41 -07:00
John Paul Soliva
2981ca24da fix(config): a refused .env write fails instead of reporting success and half-applying
The .env write lock (a package-managed install, or a key the administrator
pins in the managed-scope .env) refuses inside save_env_value /
remove_env_value by printing to stderr and returning. The caller cannot tell
that apart from a successful write, and the four multi-store writers built on
them carried on:

- save_provider_env_credential still rewrote config.yaml api_key mirrors of
  the old value, reloaded the pool and returned ok:true.
- remove_provider_env_credential still pruned the env-seeded credential_pool
  entry, recorded env:<VAR> in suppressed_sources and dropped config.yaml
  mirrors.
- save_env_setting / remove_env_setting still dropped the top-level
  config.yaml copy.

So `hermes config set|unset <PINNED_KEY>` printed the refusal, then "✓ Set" /
"✓ Unset", and exited 0. PUT and DELETE /api/env answered 200. With the user's
.env holding an old key and model.api_key mirroring it, a refused set left
.env on the old key and moved config.yaml to the new one.

The lifecycle writers now check the lock before any side effect and raise
ValueError with the refusal (require_env_writable). The CLI set/unset env
routes turn that into exit 1, and _env_write_errors already maps ValueError to
400, so the dashboard and Desktop get the reason. save_env_value and
remove_env_value keep their return shape for their other callers.

model.disconnect (TUI/Desktop) loops over every env var of a provider, so
it asks the lock about all of them (env_write_refusal) before removing any:
a pinned key refuses with nothing touched, where it used to strip the
user's own key first. A locked var that holds nothing is skipped, so
Disconnect keeps clearing the auth store on a package-managed install,
where every .env write is locked but api keys live in auth.json. The CLI
secret prompt prints the refusal instead of only the skill setup's log line.
2026-09-28 02:00:41 -07:00
kokhlo
c70374b561 fix(update): map SSL_CERT_FILE into GIT_SSL_CAINFO for git children 2026-09-28 02:00:41 -07:00
teknium1
9b02a977bd test(e2e): assert the dashboard handoff cells the respawn fixes turned green
Removes FALSE_SUCCESS_GATE (#109290) and the N-1 #124778 dashboard gate: the respawn runs in the
updated checkout's code in both columns, and the N-1 column is 7/7 on this branch (5/7 on
origin/main with the gates off). Removes PID_FILE_GATE (#123109): the bootstrap launcher is already
classified by #124965 and the cell passes on origin/main in both columns.
2026-09-28 01:52:54 -07:00
teknium1
0c397dd6af fix(update): respawn a pre-takeover dashboard through the installation-bound launcher
The pre-takeover argv is [<old venv python>, <hermes launcher>, ...]. Rebuilding it on
sys.executable + <repo>/hermes spawns a child that only works if that interpreter already has the
dependencies on sys.path; runtime_command is the launcher every other Hermes spawn uses (store
Python + bootstrap), so the respawn works from the bare takeover interpreter too. Only a
python + hermes-launcher pair is rebuilt.
2026-09-28 01:52:54 -07:00
liuhao1024
2ab313e625 fix(update): report the original dashboard argv from a failed respawn
_respawn_dashboard_processes appended --no-open to the argv before the
Popen call and returned the modified argv in the failed list, so the
receipt-side matching (pid_cmdline.get(p) in failed_cmds) never matched
a dashboard command — the #109290 gap survived for the most common
case. Keep the spawn argv separate and report the caller's original
argv on failure, which is what the cmdline reconciliation compares
against.
2026-09-28 01:52:54 -07:00
liuhao1024
cc64e36ff9 fix(update): record a failed dashboard respawn as "failed" in the receipt
When the post-update dashboard respawn raised, the receipt derived the
runtime outcome from the incarnation liveness probe: the pre-update pid
being gone is exactly what a failed respawn looks like from the outside,
so the row read "restarted" and the overall receipt read "success".
The truth already existed on the producer side (the respawn path returns
the argvs that failed to spawn) and was dropped at the call site.

Thread it through: _restart_killed_backends now returns the PIDs whose
argv respawn was attempted and failed, _finish_dashboard_update_cleanup
returns them to both update paths, and match_runtime_outcomes checks
failed_respawn_pids before the incarnation probe — "we watched the spawn
fail" outranks "gone". Reuses the existing "failed" vocabulary (the unit
path already emits it) and escalates the receipt to "partial" like the
unaccounted-tripwire class. See #109290.
2026-09-28 01:52:54 -07:00
liuhao1024
e317693749 fix(update): respawn a manual dashboard through the current install's entry
A pre-PM-takeover install left ~/.local/bin/hermes as a symlink to a
Python console script, so the kernel recorded a manual backend as
[<old venv python>, <launcher>, dashboard, ...]. After the takeover
rewrote that launcher into a POSIX shell shim, _respawn_dashboard_
processes replayed the captured argv verbatim: the old interpreter was
asked to parse a shell script, the child died at parse time
(SyntaxError in logs/dashboard-restart.log), and the updater still
printed '✓ restarted' (#124778).

Collapse any [interpreter, launcher, ...] pair onto the running
install's sys.executable + repo-root hermes entry before spawning
(other shapes, e.g. python -m, replay unchanged), and treat a child
that exits within a 1s grace window as a failed restart instead of a
success — a respawned backend is a resident server, so an immediate
exit is a startup failure (stale argv, port already bound), never a
restart.
2026-09-28 01:52:54 -07:00
Siddharth Balyan
d9d122a003 refactor(onboarding): remove the intro cinematic, the guided-onboarding tests, and their comments (#126139)
* refactor(desktop): remove the onboarding intro cinematic and the guided-onboarding tests

The guided first run now opens straight into the guide: the gate's
`cinematic` phase becomes `pending`, and OnboardingChatGate owes the guide
on mount (the old HERMES_SKIP_INTRO path, now the only path). Removes the
native intro window, its IPC bridge, the `?win=intro` renderer, its assets,
and the skipIntro launch flag. Deletes the guided-onboarding test files ahead
of the rewrite, plus the reset helpers only they called.

* refactor(onboarding): strip comments from the guided-onboarding modules

Clears comments and docstrings from the guided-onboarding files ahead of
their rewrite, desktop and backend. Lint and type pragmas stay. No code
changes.
2026-09-28 08:33:43 +00:00
Teknium
188a1a5efd Publish nousresearch/hermes-sandbox:desktop, the terminal-backend sandbox image with a display stack (#121110)
Some checks failed
Deploy Site / deploy-vercel (push) Has been cancelled
Deploy Site / deploy-docs (push) Has been cancelled
auto-fix lint issues & formatting / Generate eslint --fix patch (push) Has been cancelled
auto-fix lint issues & formatting / Apply patch (push) Has been cancelled
Skills Index Freshness Check / check-freshness (push) Has been cancelled
OSV-Scanner / Scan lockfiles (push) Has been cancelled
* feat(docker): publish nousresearch/hermes-sandbox:desktop for terminal backends

The terminal backends (docker, modal, daytona, singularity) all default to
nikolaik/python-nodejs:python3.11-nodejs20, a bare Python+Node base. For Bot
Screen, computer_use and the browser to run INSIDE that sandbox instead of on
the gateway host, the sandbox image needs the display stack.

docker/sandbox-desktop.Dockerfile is that base plus:
  - the everyday tools it lacked (jq, ripgrep, fd, tmux, less, nano, vim,
    zip, rsync, tree, procps, htop, sudo for the base's uid-1000 `pn`)
  - the exact package set the Hermes -desktop image installs (TigerVNC,
    Xfce components, dbus, xauth, fonts)
  - Playwright's headed Chromium (same build as the -desktop image)
  - cua-driver 0.28.2 from its pinned release tarball

No Hermes inside; the default user stays root like the base so nothing
changes for people who just switch docker_image. Desktop processes run as
`pn`. 4.27 GB on amd64.

docker.yml gains a `sandbox` variant with its own cache scope and repository
(nousresearch/hermes-sandbox:desktop, :main-desktop, :<release>-desktop);
the docker-integration suite is skipped for it (no Hermes to test) and
docker/sandbox-desktop-smoke.sh runs instead: as `pn`, every launcher and
cua-driver binary resolves, the real launcher.sh publishes :20, the RFB
socket completes the 3.8 handshake relayed over `docker exec -i` stdio, and
a headed Chromium maps a window on that display. hadolint lints the new
Dockerfile in docker-lint.yml.

* ci(docker): build the sandbox image on release/dispatch, not every main push

Leaves docker.yml exactly as on main. The sandbox image carries no Hermes code,
so two 4 GB multi-arch builds per merge bought nothing. sandbox-image.yml builds
and smokes on a PR that edits its own Dockerfile/smoke, and publishes only on a
release or a manual dispatch with publish=true. Stable tag stays :desktop.

* feat(docker): sandbox desktop base on python3.13-nodejs26

Matches the Hermes image (Python 3.13 / Node 26) and the top of requires-python;
the default docker_image tag it inherited was Python 3.11 / Node 20. Same pn
uid 1000, Debian 13; smoke (launcher, RFB relay, headed Chromium) passes.

* feat(docker): sandbox desktop image bakes agent-browser and Pillow

The runtime PR wraps the browser tools' agent-browser invocation in the sandbox
and grabs the Screen pane thumbnail inside it (the X socket and its cookie live
there). Both need to be in the image the default tag publishes, or the runtime
finds a tag-compatible image without them. agent-browser is pinned to the range
the gateway resolves (tools/browser_tool.py AGENT_BROWSER_NPX_SPEC), scripts off,
so the first browser_navigate in a fresh sandbox never waits on npm. The smoke
checks both as pn.

* docker(sandbox-desktop): login sessions find the browser (PLAYWRIGHT_BROWSERS_PATH via /etc/environment)

Dockerfile ENV reaches docker exec only. As the target of the ssh backend
(sshd on top of this image) a login session gets its environment from PAM,
so agent-browser reported 'Chrome not found' over ssh while working under
docker exec. Write the browser location to /etc/environment as well; the
smoke test asserts a clean login environment resolves it.
2026-09-28 01:13:26 -07:00
kshitijk4poor
58d146e961 test(gateway): cover empty/None channel prompts on eventless follow-ups
Fold #125784's prompt matrix ("Channel hint.", "", None) into the existing
parametrized continuity test. "Channel hint." is the regression guard; ""
pins the plain pass-through (downstream consumers treat "" and None alike,
so it guards against an `or None` rewrite, not a behaviour difference); None
is the control that passes on main and head and keeps the parent-override
ephemeral invariant honest. Drop the negative-control assertions that could
not fail once the override check passed (a tautological inequality and stub
echoes), and the trailing blank line that tripped `git diff --check`.

Co-authored-by: funky-xamarin <30426178+Wenfengcheng@users.noreply.github.com>
2026-09-28 13:13:50 +05:30
JoaoMarcos44
122094c568 refactor(gateway): scope eventless prompt inheritance
(cherry picked from commit 25cd0f8a6617945c824a06ae4bfe29f92dc82228)
2026-09-28 13:13:50 +05:30
JoaoMarcos44
e891ac10cb test(gateway): protect event prompt override
(cherry picked from commit cd5b7a9193938dde3cf1e75e0c5b735d57cc6b50)
2026-09-28 13:13:50 +05:30
JoaoMarcos44
ecbac1aa43 test(gateway): cover eventless prompt continuity
(cherry picked from commit 81cf22c3de009882a51ca7de2b08051858e0b091)
2026-09-28 13:13:50 +05:30
JoaoMarcos44
5646c1090a fix(gateway): preserve eventless follow-up channel prompt
(cherry picked from commit 11eac90bf589f318e1c8320fa3ba00927e643f83)
2026-09-28 13:13:50 +05:30
Siddharth Balyan
98278833e2 Compaction follows compression.threshold again: no default 256K token cap (#117915, #125235) (#126064)
* fix(compression): compaction follows the ratio again; no default token cap

compression.threshold_tokens defaulted to 256000 since #115986, so every
window above ~341K compacted at 256K regardless of compression.threshold
or model_thresholds: a 1M-window model compacted at 25% of its window,
and threshold: 0.8 still compacted at 256K. No single token count suits
windows from 64K to 1M+, so the cap goes back to opt-in (null) and the
ratio decides, as it did before #115986.

The template seeder and `hermes doctor --fix` copied the 256000 default
into config.yaml, where it reads as a user choice and would keep capping
those installs. Config v47 drops threshold_tokens only when it equals
256000; any other explicit cap and an explicit null are preserved.

The rest of #115986 stays: the model-switch warning quoting the real
trigger and the shared _derive_trigger are correct with any default.
Tests that encoded 256K now derive expectations from DEFAULT_CONFIG.

* docs(compression): threshold_tokens is an optional cap, default null

User guide, developer guide and delegation page described the 256K
default cap; they now describe the ratio trigger as the default and
threshold_tokens as an opt-in cost ceiling.
2026-09-28 07:29:40 +00:00
kshitijk4poor
5912ed81ed chore(desktop): name the bootstrap cache key and ref policy accurately
cachedScriptPath takes a cache key (often branch-main), not a commit, and
downloadInstallScript now receives the branch for existing checkouts too.
2026-09-28 12:40:15 +05:30
JoaoMarcos44
82cff2ed43 fix(desktop): keep bootstrap script aligned with checkout 2026-09-28 12:40:15 +05:30
teknium1
8a3ede1be0 fix(bundle): payload smoke runs the harness with telemetry off, like runtime
browser-harness sends each CLI event from a detached python child; in the
smoke it outlived the check and held the relocated payload open on Windows,
so the restore rename failed with EPERM. Hermes's own harness env sets
ANONYMIZED_TELEMETRY=false; the smoke now does the same.
2026-09-27 23:53:40 -07:00
teknium1
2d7235112d test(bundle): give the synthetic smoke payload a harness, and prove its absence fails 2026-09-27 23:53:40 -07:00
teknium1
2843d8eea4 test(bundle): smoke the payload's browser-harness on the store interpreter
The relocated-payload smoke now runs browser_exec's engine the way
tools/browser_use_cli.py launches it (store interpreter, -S, only the
payload site dir on PYTHONPATH), so a payload that cannot start the
default browser driver fails the release build instead of shipping.
2026-09-27 23:53:40 -07:00
teknium1
1d287d5375 fix(browser): ship the Browser Use CLI engine in every install, Desktop included
The default browser_exec tool ran the `browser-use` CLI from a PM side
environment (browser-use==0.13.10 in <home>/environments/browser-use),
provisioned by the installers and `hermes update`. Sealed Desktop payloads
skip that step, so the Desktop app never had it and silently fell back to
the built-in tools; the side env was also per-profile and 225 MB.

The CLI's execution path is only `browser_harness.run.main()`; the
browser-use agent framework (anthropic/openai/google-api pins, 93 MB of
googleapiclient) is never imported. browser-harness itself is 2.6 MB of
pure Python whose pins (Pillow 12.3.0, websockets 15.0.1) already match
Hermes's own, so it becomes a core dependency and runs on sys.executable:

- pyproject/uv.lock: browser-harness==0.1.13 (+ cdp-use, fetch-use).
- _find_cli() returns [sys.executable, -m, browser_harness.run]; the child
  env points PYTHONPATH at the harness site dir (the Desktop store
  interpreter boots without a venv and the harness daemon re-runs
  sys.executable), replacing whatever the agent inherited.
- The side-env provisioning (install_cli, the update/installer step) goes.
2026-09-27 23:53:40 -07:00