Unifies the two gateway subsystems that were fighting each other: the
'never lose a session' recovery machinery (#54878 stale-route self-heal,
find_latest_gateway_session_for_peer reopening agent_close/ws_orphan_reap
rows) and the session reset/expiry machinery (expiry watcher, /new,
/resume, resume_pending freshness gate).
The unified contract:
- INTENTIONAL boundaries (expiry finalization, auto-reset, /new,
/resume switch) are recorded durably via promote_to_session_reset(),
which upgrades accidental recoverable end_reasons (agent_close,
ws_orphan_reap) to the explicit boundary while preserving other
explicit reasons (compression, etc.). Recovery then correctly refuses
to resurrect them.
- ACCIDENTAL ends (crash, cleanup bug, mistaken reaper) stay
recoverable — genuine crash recovery is untouched.
On top of the cherry-picked contributor commits:
- promote_to_session_reset widened to ws_orphan_reap + parameterized
reason so auto-reset paths stay auditable (idle/daily/suspended/
resume_pending_expired) (#61220, #61993, #63539)
- get_or_create_session auto-reset, reset_session (/new), and
switch_session (/resume) all write through the promote path — the
first-reason-wins end_session no-op could previously leave a reset
session resurrectable behind a stale agent_close row (#61993)
- resume_pending freshness gate now honors session_reset.mode=none:
explicit opt-out of automatic resets also opts out of the zombie
gate (#61052)
- resume recovery note extracted to build_resume_recovery_note() and
made adapter-aware via a new interactive_resume capability flag:
webhook/api_server auto-resume turns now CONTINUE the interrupted
task instead of emitting an unanswerable 'session restored'
acknowledgement that abandoned the work (#57056)
- tests updated to call the real note builder instead of mirroring it
E2E-validated against a real SessionDB + SessionStore in a temp
HERMES_HOME: expiry->agent_close->no-resurrection, /new promote,
crash recovery preserved, mode=none opt-out, routing-table flag sync.
Only advertise finite watchdog deadlines that are still in the future, exercise the full MoA heartbeat path, and register the salvaged contributor attribution.
Commentary delivery is on by default; users who find the extra mid-turn
narration noisy can set display.show_commentary: false to restore the
previous behavior (commentary routed to the reasoning channel, visible
only with show_reasoning).
- hermes_cli/config.py: display.show_commentary default true
- agent/agent_init.py: wire config -> agent.show_commentary
- run_agent.py: gate structured commentary extraction on the flag
- agent/codex_runtime.py: gate live-stream commentary callback (falls
back to legacy reasoning-channel routing when off)
- docs + 2 tests (interim path off, live stream fallback)
Also adds AUTHOR_MAP entries for davidrobertson and 100yenadmin.
Add a runtime_validator callback to generate_title() / auto_title_session()
/ maybe_auto_title(). Callers snapshot the session's model+provider when
spawning the background titler; the validator runs right before the LLM
request and skips it silently when the live runtime no longer matches —
so a stale title request can't reload a model that strict_single_load
already evicted after a user model switch. Fail-open: a raising validator
never disables titling.
Wired at all four call sites (cli, gateway, tui_gateway, acp_adapter).
Surgical reapply of PR #19137 (base was 8k+ commits stale; the original
patch predates the pinned-language prompts, the atomic-write helper, and
the moved TUI/ACP call sites). Original work by @Thatgfsj. Closes#19027.
Combines the two salvaged fixes so they compose instead of conflict:
_persist_session_title (#50575) now writes through set_auto_title_if_empty
(#51483) when the store provides it — the collision-dedup retry and the
manual-/title race protection apply together. Predicate failure (a manual
title landed while generation was in flight) returns None: nothing written,
no callback. Legacy stores without the atomic method keep the plain
set_session_title path, including the vanished-session RuntimeError.
Tests cover both store shapes plus the race-skip path; E2E verified against
a real SQLite SessionDB (collision -> 'Weekly Report #2', manual title
preserved, cron dedup, blank guard). AUTHOR_MAP entry for rasitakyol.
Follow-ups for the consolidated salvage:
- Memoize the config.yaml-derived timeout on the file's mtime_ns so the
rebuild-on-timeout-change check from PR #57437 costs one stat() per
get_honcho_client() call instead of a full YAML load on the hot path.
- hermes honcho status now displays the host-block-resolved
dialecticCadence (remnant from PR #63776, whose runtime fix landed
in #62290).
- AUTHOR_MAP entries for the salvaged contributor emails.
- Make the config imports lazy inside _auto_title_enabled(), matching the
existing _title_language() pattern (title_generator is imported from agent
code paths where a module-level hermes_cli import risks circularity).
- Check the enabled flag after the cheap first-exchange guard in
maybe_auto_title so config isn't read on every turn of a long session.
- Repoint the two new tests at the real import site.
- Document the key in cli-config.yaml.example and merge the enabled flag
into the existing title_generation block in configuration.md.
- AUTHOR_MAP entry for the contributor.
* fix: detect env-var-configured providers absent from PROVIDER_REGISTRY
is_provider_explicitly_configured() only checked PROVIDER_REGISTRY (a
manually-maintained dict) for env-var names. Providers that exist solely
in the models.dev catalog — e.g. openrouter — were never recognised as
explicitly configured, so they were filtered out of the desktop model
picker even when their API key was set in .env.
Add a fallback to get_provider() (which reads the models.dev catalog)
when PROVIDER_REGISTRY returns None. Both ProviderConfig and ProviderDef
expose .auth_type and .api_key_env_vars with the same shape.
* test: keep OpenRouter provider gate assertion behavioral
* chore(release): map salvaged OpenRouter contributor
---------
Co-authored-by: zzpigpinggai <zzpigpinggai@users.noreply.github.com>
Add the flag-gated compute-host supervisor, delta/control protocol, PPID orphan guard, inline fail-open path, synthetic GIL-heavy turn seam, and AC-4 certify harness.
Verified on current origin/main: 343 focused tests pass; Ruff and diff checks pass; 360s AC-4 run with six heavy lanes passes at 6.11ms serving p99 with zero stalls and valid load.
Co-authored-by: Kyzcreig <9063726+Kyzcreig@users.noreply.github.com>
The salvaged choices list predates the max/ultra effort levels (#62650);
add them so the Discord dropdown matches the canonical ladder. Discord
caps choices at 25 — we're at 11, plenty of headroom.
_clean_reasoning_effort kept its own whitelist that stopped at 'max',
silently dropping 'ultra' from MoA slot configs. Route it through
hermes_constants.parse_reasoning_effort — the same one-source-of-truth
fix the salvaged commit applies to the gateway — so future effort
levels can't drift here either. Docs updated to list ultra.
Follow-up to salvaged PR #64012.
Follow-ups on top of the cherry-picked cluster commits:
- slack: scope-authoritative app-token read — get_secret() with a
narrow UnscopedSecretError fallback to os.getenv. Keeps @kohoj's
correct semantics (scoped profile can never silently inherit the
default profile's Socket Mode app) while fixing the regression where
the default-profile startup loop and background reconnect rebuild,
which call connect() unscoped under multiplex, would raise and
fail-loop. Supersedes the 'or os.getenv' variant from #64461 which
reintroduced the cross-profile fallback leak.
- test: unscoped-multiplex fallback regression test for connect().
- run.py: convert the last legacy self.adapters.get(source.platform)
site (_rename_discord_auto_thread) to _adapter_for_source(source)
so profile-routed Discord sources rename threads on the right
adapter (from #57417's sweep).
- AUTHOR_MAP entry for @aguung.
Follow-up on the salvaged #64611 commit: the original guard blocked the
install tree unconditionally, which would have broken the legitimate
'developing Hermes from a source clone' CLI flow (launching hermes inside
the repo and getting its AGENTS.md as project context).
Refined policy:
- resolve_context_cwd(): validates configured paths (missing dir -> None +
warning) but honors an EXPLICIT install-tree cwd verbatim — deliberate
user choice.
- build_context_files_prompt(): blocks only the cwd=None -> os.getcwd()
FALLBACK into the install tree, with a new allow_install_tree_fallback
param. system_prompt.py passes it for platform cli/tui (launch dir is
the user's real shell cwd there); desktop/gateway surfaces keep the
guard (their fallback dir is self-spawned, never user-picked).
- Warning log names the resolved dir and the terminal.cwd remedy.
E2E-verified all five scenarios: desktop fallback blocked, in-tree CLI dev
keeps AGENTS.md, explicit install-tree cwd honored, invalid TERMINAL_CWD
falls to None then blocked, normal workspace loads.
Follow-ups to @SAMBAS123's #64986 salvage:
- Replace the hardcoded token-platform set in _platform_has_bot_credential
with PLATFORM_TOKEN_ENV_NAMES, a shared canonical map in gateway/config.py
also used by the empty-token validation warning — one source of truth, so
future token platforms can't silently bypass the gate or drift between
the two sites.
- After secondary-profile startup, warn loudly for any platform skipped on
the primary that no secondary profile ended up serving: an enabled
platform with no credential anywhere is a config error, not a silent
no-op.
- AUTHOR_MAP entry for the salvaged commit's author email.
git diff on a lockfile is unreadable: npm reorders entries, rewrites
integrity hashes, and moves packages between nesting levels, so a
one-line package.json bump produces a thousand-line textual diff.
scripts/ci/lockfile_diff.py instead parses the `packages` map out of
both versions of every tracked package-lock.json (via `git show`),
reduces each to {install path: version}, and set-diffs the maps —
reorder/hash churn vanishes, leaving only actual version movement
(added / removed / updated, with nested dedup copies tracked
separately).
The lockfile-diff workflow posts the result as a Markdown table in a
PR comment gated behind a hidden marker: subsequent pushes PATCH the
existing comment instead of stacking new ones, and a push that reverts
all lockfile changes updates the comment to say so. Advisory only —
never fails on findings; fork PRs (read-only token) degrade to a
warning.
Wired through the ci.yml orchestrator with a new npm_lock lane in
classify_changes.py (fails open on .github/ changes per the existing
contract).
* fix(nix): fold makeWrapper line continuations into optionalStrings
When rev == null (any dirty-tree build), the empty optionalString
expansion left the previous line's trailing backslash dangling onto a
blank line, ending the makeWrapper command early and running
`--suffix PYTHONPATH ...` as its own shell command (`--suffix: command
not found`, exit 127). Clean trees passed CI; dirty trees with
extraPythonPackages failed — exactly the path the NixOS module
exercises.
The continuation now lives inside each optionalString (" \\\n --set
..."), so the makeWrapper chain stays intact whether or not the
optional flags expand.
Verified by building with rev = null + extraPythonPackages =
[ pyfiglet ]: wrapper builds, PYTHONPATH suffix lands inside the
makeWrapper call, wrapped `hermes --version` runs, and the collision
check still executes (certifi correctly rejected).
* perf(nix): filter derivation sources to shrink rebuild scope
Every derivation previously saw the whole repo, so any file change
rebuilt everything. Each derivation now gets a filtered src with only
the files it consumes:
- lib.nix: derive npm workspace topology from the root package.json
`workspaces` globs (single source of truth — a new workspace member
is picked up with zero nix edits). pythonSrc (cleanSourceWith)
excludes the JS workspace trees, docs/website, docker/.github,
tests, nix/, flake.nix/flake.lock, root docs, and skills/ +
optional-skills/. importNpmLock reads from a fileset-filtered
npmRoot (root manifests + member package.jsons only).
- mkNpmPassthru takes `dirs` — the workspace dirs the package
contains — and builds a per-package fileset src from them. web and
desktop include apps/shared (file: dep). One shared
`nix run .#update-npm-lockfile` replaces the per-package
update_*_lockfile bins that only existed inside build sandboxes.
- python.nix: release venv loads the uv2nix workspace from pythonSrc.
The editable venv keeps an unfiltered ./.. root —
mkEditablePyprojectOverlay calls lib.path.splitRoot, which rejects
a cleanSourceWith set, and the editable install reads the live
checkout anyway.
- hermes-agent.nix: skills ship exclusively via HERMES_BUNDLED_SKILLS
/ HERMES_OPTIONAL_SKILLS (same mechanism as Homebrew packaging;
setup.py's _data_file_tree returns [] for missing dirs), so
SKILL.md edits no longer rebuild the venv. optional-mcps stays in
the wheel — pyproject.toml lists its manifests as explicit
data-files. Bundled assets are symlinked instead of copied, making
the wrapper drv near-instant when only an input changed.
__pycache__ filtered from bundled skills.
- checks.nix: find -L through the new symlinks; assert
optional-skills presence + HERMES_OPTIONAL_SKILLS in the wrapper.
- run_tests.sh: fall back to $HERMES_PYTHON when no local venv
exists, guarded by an `import pytest` probe (HERMES_PYTHON from a
wrapped hermes binary points at the release venv, which has no
pytest — without the guard every test file dies with "No module
named pytest" while the runner exits 0).
Verified: nix flake check exit 0; built .#default .#tui .#web
.#desktop; SKILL.md and flake.nix edits leave the venv drvPath
unchanged; .py edits leave the tui drvPath unchanged; .tsx edits
leave the venv drvPath unchanged (and do change the tui drv);
scripts/run_tests.sh runs 299 tests green through both the venv and
HERMES_PYTHON paths, and rejects a pytest-less HERMES_PYTHON.
* refactor(nix): overlay aliases the flake's own package instead of re-instantiating
The overlay previously re-called callPackage against the consumer's
nixpkgs (final), so pkgs.hermes-agent could be a different derivation
than nix build .#default and the NixOS module's default — an untested
build matrix against arbitrary consumer nixpkgs versions, for a
package whose Python side is uv2nix-locked anyway.
Now the overlay is a pure alias for the flake's own locked package:
one callPackage site (packages.nix), everything else references it.
.override { ... } still works — callPackage's makeOverridable travels
with the derivation.
Verified: direct drvPath == overlaid drvPath; .override produces a
distinct drv.
* fix(nix): dedupe extraPlugins assertions, replace MESSAGING_CWD with terminal.cwd
- Delete the duplicated extraPlugins duplicate-name assertions block
(same assertion declared twice back to back).
- Stop setting the deprecated MESSAGING_CWD env var, which made the
module trip hermes' own startup deprecation warning. The working
directory is now injected as terminal.cwd into the generated
config.yaml; cfg.settings wins via recursiveUpdate, and container
mode maps to the in-container mount path.