0b74f322e2ccbf886a33124ff00181ea1833d4a4
812 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
0b74f322e2 |
fix(profiles): --clone copies the active memory provider's config (#120115)
--clone carried memory.provider (e.g. hindsight) in config.yaml but not the provider's own config under the profile home (hindsight/config.json, mem0.json, ...), so the clone booted with the provider selected and silently unavailable. Copy the ACTIVE provider's <provider>/ dir and/or <provider>.json by the same convention the dashboard memory-provider routers read, guard the name against path traversal, tighten copies to 0600 (they can hold an API key), and say so in the CLI notice. Convention-based on purpose: hindsight is a catalog plugin now, so a hook the plugin must implement could not fix the reported case, and no provider module is imported during profile create. Supersedes #43107 (credit @bionicbutterfly13 for the direction). |
||
|
|
c500dc6d99 |
fix: hermes update restarts only the gateways of the home it updates (#93349)
hermes-gateway*/hermes-serve* units, ai.hermes.gateway* LaunchAgents and `gateway run` processes are account-wide namespaces shared by every Hermes install on the box. The restart phase enumerated them by name, so a scratch home's `hermes update` drained and restarted the account's real hermes-gateway.service and SIGTERMed sibling installs' gateways (live: #93349 comment, 2026-09-23), then warned "Fleet version check returned no rows" because none of those runtimes belonged to the updating home. Ownership is now judged from what a runtime actually runs on, never from the label: the live process environment (`_hermes_home_for_pid`), the unit's declared Environment=HERMES_HOME, or the plist's pinned HERMES_HOME, compared against the homes the update's plan inventories (the updating root and its profiles/<name>). Foreign or unreadable ownership is named in the output and left alone; it is not a failed restart. Applies to the systemd fleet loop, the catch-up best-effort restart, the launchd derived-label loop, the manual gateway sweep, and the pre/post-restart PID snapshots that drive the fail-closed verdict. |
||
|
|
24f03c41c1 |
fix(agent): hosted providers no longer get the local-server "wait and /retry" context rejection
The unexplained-rejection gate from #114644 ended the turn with "another request on the same server was probably holding its capacity ... wait and /retry" whenever a server said "context exceeded" without a count while the local estimate sat under half the known window. That cause only exists on single-slot local servers. On a hosted route (Anthropic, Nous, OpenRouter, any public endpoint) the same rejection means the route's real window is smaller than the one Hermes assumes, so /retry failed identically every turn and the conversation was never compressed: Discord bots on claude-opus-5-5 were stuck repeating the message. The gate now also requires is_local_endpoint(base_url) (loopback, LAN, Tailscale, container DNS). Hosted endpoints return to the compress-and-retry path they had before #114644. The FAQ entry says which endpoints get the message. |
||
|
|
9d799e0531 |
docs: hindsight installs from the plugin catalog
memory-providers.md points the Hindsight section at the catalog entry and the upstream integration docs, documents the auto-install on `hermes update` / first agent start (and the allow_lazy_installs=false one-liner), and adds a "Migrating from bundled Hindsight" subsection. memory.md, overview.md, integrations/index.md, cli-commands.md, docker.md and nix-setup.md no longer say hindsight ships in-tree or as the [hindsight] extra. |
||
|
|
550d74c62f |
fix: manage_catalog documented under its own setup toolset; post-hook ownership contract exercises it
tools-reference.md listed manage_catalog under the connections section, which the reference-docs contract resolves against the connections toolset. The agent-runtime post-hook ownership test enumerates every tool in AGENT_RUNTIME_POST_HOOK_TOOL_NAMES; manage_catalog now runs through both executor paths there. |
||
|
|
31e59b9441 |
feat: setup agent can search the catalog and install plugins and skills through the approval card
The setup profile's `setup` toolset was empty. It now carries one tool, manage_catalog: - search: catalog plugins (the Plugins tab's live catalog resolver) and hub skills, with whether each is already installed in the default profile. Read-only. - install: opens the same connection operation manage_connections opens, with rows of kind plugin / skill. Nothing installs until the user approves a row. An approved row installs into `default` (or the profile the Advanced modal named) through dashboard_install_plugin / the hub's headless install, so the catalog pin, kill list, security scan and live activation (#119644) are the host's. The row settles with the live MCP tool names and the plugin's skill. The model sends catalog ids and an action only; every other key is refused before anything runs. An unknown id or a plugin this OS cannot run is drawn failed with the installer's own text. Anywhere a catalog card cannot be drawn (TUI, CLI, messaging, registry dispatch) the result is the `hermes plugins install` / `hermes skills install` pointer. - contract: plugin/skill targets follow the MCP transitions. - run.apply_answer / reissue route a card answer to the module that owns the operation. - tool_search: `setup` joins the direct-surface toolsets, so the guide's one tool is never deferred behind tool_search. - docs: tools reference, toolsets reference, plugin catalog page. Linear NS-964. |
||
|
|
333898b353 |
feat: setup profile sessions get the setup toolset by profile role; nothing else can grant it (#119491)
The setup toolset (empty until NS-964 registers request_catalog_install) is reserved for the profile whose backend-written profile.yaml carries role: setup. Two points enforce it: - Grant: tui_gateway/server.py::_load_enabled_toolsets folds the in-scope profile's role toolsets into all three return paths (configured CLI toolsets, coding posture, HERMES_TUI_TOOLSETS pin), next to the client-surface set. The pin keeps it too: an operator pin picks configurable toolsets and must not strip the profile's own. - Deny: model_tools._select_tool_names strips toolsets reserved for any other role from every selection, including None/"all", a saved platform_toolsets list, profiles.configure and the env pin. That is the one point every surface's selection passes, so a default session cannot get the tool by any route. The role is read with read_profile_meta on get_hermes_home(), which under a session's home override is that session's profile dir (no directory scan). setup stays out of _HERMES_CORE_TOOLS, CONFIGURABLE_TOOLSETS and the platform-native recovery loop (it has no tools, so the loop skips it). Linear NS-963. |
||
|
|
b05c5d1e4f |
docs(discord): say the liveness threshold only gates soft signals
Since the first-strike escalation, a closed transport (socket_closed / client_closed) forces the reconnect on the first unhealthy sample and the failure threshold only applies to soft signals (ack staleness, latency, event silence). The user guide (website/docs/user-guide/messaging/discord.md:89) and the env-var reference (website/docs/reference/environment-variables.md:780) still described the threshold as gating every unhealthy sample, so an operator reading `1/2` followed by a forced reconnect would think the knob was ignored. One sentence each, citing #118487. |
||
|
|
9a2d96ca11 |
fix(config): flag quoted list/mapping values in doctor + startup, guard the list slots config set missed
A list/mapping slot holding ONE quoted string (`plugins:\n enabled: '["a","b"]'`, `model_catalog:\n excluded_providers: '["openai-api"]'`) is skipped by every isinstance-gated reader (`plugins_cmd._config_name_set` -> set(), `plugins._names`, `inventory.excluded_providers` -> []) while `config get` echoes it back, so user plugins silently unmount and provider exclusions silently lapse. Neither `hermes doctor` nor the startup `print_config_warnings` banner said a word. Reader/doctor half: one schema-aware pass in `validate_config_structure` (`_validate_quoted_containers`) walks `DEFAULT_CONFIG` (sections included) plus `_KNOWN_CONTAINER_TYPES` and warns when the user value is a string that parses to a list/mapping. The message names the key, the quoted value and the remedy (`hermes config set <key> '<literal>'`). Finding only: the file is never rewritten. String-typed keys (`approvals.mode: "[off]"`), the `model: <name>` shorthand and the `parse_config_string_list`-read slots (`agent.disabled_toolsets`, `skills.disabled`) are not flagged. Feeds both the doctor "Config Structure" section and the startup banner. Writer half (audit of every path that can put a string in a container slot): - `hermes config set` (`hermes_cli/config.py::set_config_value`): already parses bracket/brace literals (#88163) and refuses wrong-shaped values (`_refuse_container_type_mismatch`), BUT the guard only knew slots present in DEFAULT_CONFIG or `_KNOWN_CONTAINER_TYPES`. `plugins.enabled`, `plugins.disabled` and `model_catalog.excluded_providers` are deliberately absent from DEFAULT_CONFIG, so `config set plugins.enabled foo` / `plugins.enabled a,b` / `model_catalog.excluded_providers openai-api` stored a plain string (live repro on base). Added the three keys to `_KNOWN_CONTAINER_TYPES`: those writes are now refused with the literal hint. - `cli.py::save_config_value` + callers (cli_*_mixin, gateway/slash_commands, gateway/run_busy): every caller passes a bool/enum string for scalar keys; no list-slot caller. Not reachable. - `hermes_cli/plugins_cmd.py::_save_plugin_sets` / `_write_config_value` and `plugins_cmd_catalog` (via `_save_enabled_set`): write `sorted(set)` — real lists. Not reachable. - Dashboard `PUT /api/config` (`web_routers/config_env.py::update_config` -> `_denormalize_config_from_web` -> `save_config`): schema-driven form; `web/src/components/AutoField.tsx` splits list-typed fields into a real array before the PUT. Not reachable. - tui_gateway `config.set`: fixed `_CONFIG_SETTERS` table of scalar keys only (out of this lane's files anyway). Not reachable. Conclusion: the quoted shapes on real machines are leftovers of pre-#88163 `config set` runs plus the DEFAULT_CONFIG-absent slots fixed here. Live repro (fake HOME/HERMES_HOME, both quoted shapes in config.yaml): before: validate_config_structure() -> []; startup banner silent; doctor has no Config Structure section; _config_name_set('plugins','enabled') -> set() after: two warnings naming plugins.enabled / model_catalog.excluded_providers with `hermes config set ... '["a","b"]'`; doctor prints them under Config Structure; running the remedy yields real lists, validate -> [], _config_name_set -> {'a','b'} writer: `config set plugins.enabled a,b` wrote `enabled: a,b` on base; now refused ("must be a list ... nothing was written"). Fixes #83308 Fixes #105706 credit: @fangliquanflq #105725 (schema-aware detection in validate_config_structure; slim redo) credit: @Luna161 #83313 (first report + per-key warning in plugins_cmd) |
||
|
|
77f80d31ee |
fix(plugins): catalog provenance is installer-owned; kill list covers update/enable/load; re-pin keeps user files
Catalog trust bugs from the 2026-09-21 plugin audit (lane 3, F1-F6, F9): - F1 (high): a URL-installed repo shipping its own .hermes-catalog.json rendered as catalog:official everywhere and marked the real entry "installed". Provenance now lives on the installer-owned .install-metadata.json record (catalog block written by _install_plugin_core, sha = checked-out commit); read_catalog_sidecar never reads the tree. Pre-fix installs are adopted once when the installer record agrees (pinned at the sidecar sha, cloned from the entry's repo). - F2: removed.yaml bypassed by git@/ssh:///http:///www. spellings. _normalize_repo canonicalises to host/owner/repo (scheme, user, www., .git, slashes dropped). - F6: removed.yaml consulted for INSTALLED plugins too: git-pull update, enable and gate_manifest (load) refuse recalled plugins, offline (in-tree + cached live list). --allow-removed is recorded on the install record and exempts it. - F5: install NAME --ref X recorded the catalog pin, so list/TUI/update claimed the reviewed pin while HEAD differed. Recorded sha is the checked-out one. - F3: re-pin replaced the whole tree, losing the installer-created config.yaml, data and user patches with no warning. Untracked/ignored files are carried into the new tree; edits to tracked files are copied to <HERMES_HOME>/plugins-backup/<name>-<sha8>/ with a warning. - F4: a manifest rename between pins left the OLD dir installed and enabled. The stale dir is removed and the enabled flag follows the new name. - F9: dashboard payload `removed` list now includes live removals. repin_catalog_plugin returns RepinResult(sha, changed, installed_name, warnings); CLI, dashboard and TUI callers surface the warnings and the new name. |
||
|
|
e636aedebf | fix: preserve known file baselines across partial rereads | ||
|
|
65b0ff38ea |
feat(terminal): heartbeat notifications for long background processes
`terminal(background=true, heartbeat=N)` emits a `heartbeat` event every N seconds (floor 60) carrying only the output produced since the previous one, plus the usual completion notice. The agent stays current on a long bounded job (merge train, full test suite, deploy) and reacts to a failure within N seconds instead of at exit. Why: `notify=true` fires once at exit, so multi-hour merge trains ran silently — the parent's prompt cache went cold and every "what's up" cost 5–10 rediscovery calls; 30 days of batch sessions show 4,930 hand status polls (14% of all tool calls). `notify=[pattern]` cannot serve this: its lifetime cap (8) exists precisely to stop periodic output from flooding the agent. Mechanism: one daemon timer thread for every heartbeat session (reader threads block on the pipe and cannot keep time); delta output via a total-ingested counter over the rolling buffer; heartbeats only where the completion notice can be delivered (same async-support/subagent gates), never after exit. `heartbeat_seconds` is checkpointed. Rendered on CLI, gateway and TUI through the existing process-notification paths. |
||
|
|
91d1339a8d |
docs: multi-profile gateway pages match the multiplex-only topology
Every line still promising that `gateway.multiplex_profiles: false` keeps per-profile gateways, or documenting the deleted `migrate --standalone` rollback, now describes the shipped behaviour: one gateway per host serves every profile; `false` no-ops with a warning; `hermes update` folds a fleet unless a real boundary (different UNIX user, HERMES_HOME outside profiles/) holds; a blocked fleet keeps `--force` per profile; re-running `migrate --multiplex` converges a half-migrated host; a manifest on disk is the resume record, never a rollback. Adds the new "No new per-profile gateways" section with the exact refusal `hermes -p <name> gateway install` prints. Files: website/docs/user-guide/multi-profile-gateways.md, website/docs/reference/cli-commands.md (migrate row), website/docs/developer-guide/multiplexing-gateway.md (eager activation no longer honours `false`), hermes_cli/AGENTS.md (migrate + refusal seams). Completes #118273 (docs item). Part of #109417 |
||
|
|
661d73f06b |
test+docs: trim doctor exit-status tests to two invariants; document the exit code
Keep the in-process return-code matrix (issues / manual / --fix full and partial repair) and the real-process exit-status check; drop the exception passthrough, --ack and --live cases, which pin behaviour this change does not touch. Document 0/1 exit status under `hermes doctor` in the CLI reference. |
||
|
|
2520eb7c0d | docs(backup): list the browser profile dirs hermes backup excludes | ||
|
|
afc3b7c6f3 |
feat(connectors): one backend-owned connection operation, with a setup card on Desktop, TUI and CLI (#111008)
* feat(connectors): the desktop connects apps through one backend-owned operation Re-based onto main after #109517, #110368, #110574 and #110843 landed as squash merges ( |
||
|
|
86a599cbae |
fix(gateway): human-delay pacing comes from each profile's human_delay config, not process env
BasePlatformAdapter._get_human_delay read HERMES_HUMAN_DELAY_MODE/_MIN_MS/_MAX_MS from the process environment at every send, so under multiplexing the launch profile's pacing applied to every served profile, and the documented `human_delay:` config section (mode/min_ms/max_ms, already in DEFAULT_CONFIG) was never consulted. The runner now resolves `human_delay` per profile through the same seam as the busy-text timings (`_human_delay_from_config`, snapshotted in `_snapshot_profile_busy_modes`, installed by `_wire_adapter_handlers`) and the adapter only consumes the installed range. Invalid `custom` bounds (non-integer, negative, inverted) warn naming the key and fall back to the natural range. Fixes #116895 |
||
|
|
522e121e90 |
fix(desktop): pin the update-check proxy deps exactly and document the proxy variables
The salvaged commit added https-proxy-agent, proxy-from-env and @types/proxy-from-env as caret ranges; the repo pins every dependency to an exact version so `npm ci` resolves the same tree everywhere. Pins are the versions the lockfile already resolved (7.0.6 / 2.1.0 / 1.0.4), regenerated with `npm install --package-lock-only`. Documents that the Desktop update check now follows HTTPS_PROXY / HTTP_PROXY / NO_PROXY in the environment-variables reference. |
||
|
|
dbcbd9d9db |
feat(gateway): /branch opens a sibling thread by default; --here keeps this chat (#66023)
On Discord, Telegram, Slack and Matrix a plain `/branch` used to rebind the CURRENT chat/thread's session key to the clone, ending the original session on that surface. The user could not keep the original path live while exploring an alternate one — the opposite of what a branch is for. Now the handler opens a sibling thread through the adapter's existing `create_handoff_thread` BEFORE cloning (a failed create never orphans a branch row), binds the thread's own session key to the clone with the thread's routing columns written at create time, and leaves the origin key untouched. `/branch --here` keeps the legacy in-place switch; platforms without threads, DMs, unknown Discord parents and adapters that cannot open a thread fall back to in-place with a one-line note. The CLI strips the flag through the same parser so `--here` never becomes a session title. Destination source shapes mirror each adapter's inbound key (Discord keys threads on their own id; Telegram/Slack/Matrix on the parent chat), the same rules the CLI->platform handoff uses. Live repro (real gateway + real Slack adapter against a stand-in Slack Socket Mode/Web API): base ends the origin session and rebinds its key; fixed posts the thread seed, replies "this chat stays on it", the origin thread keeps its session and the follow-up typed in the new thread lands on the branch (parent_session_id = origin). Design and first implementation by Angello Picasso (#66014, #66024); this is a slim port onto the split slash_commands_* layout. Co-authored-by: Angello Picasso <angello.picasso@devsu.com> |
||
|
|
6ae4cb88d5 |
fix(docs): reference pages name tools the registry never registered
The shipped tool-surface references still document the pre-consolidation surface: tools-reference.md lists cronjob/todo/process/project_create/ project_list/project_switch/open_preview/close_preview/read_preview/tour/tip (6 uncallable, 5 hidden dispatch-only aliases), and toolsets-reference.md still claims web_search is a member of the browser toolset — membership |
||
|
|
2a1ec15a6f |
docs(website): regenerate the skill docs from the shipped skill tree
website/scripts/generate-skill-docs.py is the documented source of both catalogs and of the per-skill pages, but nothing compares its output with what is committed, so the committed copies drifted: - optional-skills-catalog.md was missing agent-merge-conflict-arbiter and listed pr-lens under blockchain (it lives in software-development); - skills-catalog.md still listed merge-reconciler, which #98539 moved out of the bundled set; - 196 pages and both catalogs carried Windows path separators — in the Path column and inside GitHub blob links, where a backslash is a broken URL. This commit is the generator's output (re-running it on this branch is a no-op), plus the orphan page #98539 left behind for merge-reconciler: no skill backs it, its catalog row is gone, and no page links to it. tests/skills/test_skill_docs_contract.py is the guard: the next skill that ships without a catalog row, or a page regenerated on Windows, fails there instead of on the published page. |
||
|
|
e23a033e85 |
docs(matrix): surface the DM-classification bypass in the operator-facing docs
The prior commit only documented the <=2-member DM auto-classification
in the adapter.py module docstring — source an operator configuring
MATRIX_REQUIRE_MENTION etc. would never read. Issue #114733 explicitly
asks for the callout wherever MATRIX_ALLOWED_ROOMS,
MATRIX_FREE_RESPONSE_ROOMS, MATRIX_REQUIRE_MENTION, and MATRIX_AUTO_THREAD
are documented for operators, i.e. the env-var reference table and the
Matrix user guide. Add the same rule + bypassed vars + escape hatch to
both, matching the precedent set by
|
||
|
|
d5c2e0fb16 |
fix(env): a parent-injected dashboard session token survives the .env reload
A launcher that spawns `hermes dashboard` (Desktop shell, link-style integrations) mints HERMES_DASHBOARD_SESSION_TOKEN into the child's environment and keeps the same token for its own /api probes. Every dotenv layer in load_hermes_dotenv() loads with override=True, so a persisted HERMES_DASHBOARD_SESSION_TOKEN line in ~/.hermes/.env replaced the injected token: the child authenticated with the persisted value and the parent got HTTP 401 from its own child. Treat the token as a spawn credential in the dotenv publisher: a value that dotenv did not put into os.environ (tracked by _DOTENV_PUBLISHED) is left alone, while a value an earlier pass published still reloads, so .env edits and home switches behave as before. Other keys, including the documented HERMES_DASHBOARD_PUBLIC_URL, keep .env-wins precedence. Co-authored-by: liuhao1024 <sunsky.lau@gmail.com> Co-authored-by: fangliquan <fangliquan@qq.com> |
||
|
|
c7e165a068 |
docs: list HERMES_LANGFUSE_MAX_DEPTH in the environment-variable reference
The knob is user-facing; the README alone is not where operators look. |
||
|
|
05c4c40219 | docs(mcp): oauth.user_agent default, redacted token-error excerpt, login keeps server metadata | ||
|
|
0818892db3 |
fix(mcp): accept an origin-issued metadata document for a path-scoped OAuth authorization server
A protected resource may advertise a path-scoped authorization server (`https://www.strava.com/mcp-issuer`) whose RFC 8414 document, served from `/.well-known/oauth-authorization-server/mcp-issuer`, declares the origin (`https://www.strava.com`) as its issuer. The SDK's exact-string check (`validate_metadata_issuer`, RFC 8414 §3.3) rejected that document with "Authorization server metadata issuer mismatch" and the connection parked before registration or login (#116233). `metadata_issued_by_origin` accepts exactly that shape and nothing else: the document must have been read from the well-known URL derived from the advertised identifier (so a redirect target, the root document or an OIDC fallback never qualify) and its issuer must be the advertised identifier's origin. Only the origin's operator controls that location, so a party controlling a path or a sibling host cannot use it to make the client accept another server's endpoints; whoever could would already control the exact-match document too. The browser flow applies it in the mixin's request pump (installs the document, hands the SDK a 204 so its loop stops) without touching `auth_server_url`, so SEP-2352 credential binding keeps the advertised identifier while the RFC 9207 `iss` check and refresh-token issuer binding use the document's issuer. The device flow applies the same rule in its own discovery and now binds registered credentials the way the SDK's Step 4 does, so the runtime flow reuses them instead of discarding them on the next 401. Supersedes #116359: its rule accepted the origin issuer from any discovery URL (root and OIDC fallbacks, redirect targets) and fabricated a 500 response. Co-authored-by: Finn763 <165816600+Finn763@users.noreply.github.com> |
||
|
|
30de041b01 |
docs: explain the three gateway connection-failure replies
The chat reply now differs for an interrupted connection, a refused/unroutable endpoint and a cause-free SDK connection error; the FAQ names each wording and what to do about it so a user reading one on Telegram/Discord/Slack knows whether to restart the model server or just /retry (#116323). |
||
|
|
b6f8f8eb1f |
Merge pull request #116345 from NousResearch/boa-w3-small-b
Video generation tools no longer let the agent pick the model; video_gen.model is the only selector (Refs #83080) |
||
|
|
d180fc4311 |
Merge pull request #116340 from NousResearch/feat/codex-browser-pkce-login
Codex login gains an opt-in browser PKCE flow on localhost:1455; device code stays default (#95743, salvage #97058) |
||
|
|
19b29df13b |
fix: video generation tools no longer let the agent pick the model
video_generate advertised an optional `model` argument (and the xAI edit/extend tools a model override) so the LLM could route a single call to a different model family — a different endpoint and billing tier — than the one the user selected in `hermes tools`. image_generate never exposed this, and #83080 asked to extend it there; the ruling is the opposite: models do not choose models. The `model` property is gone from the static and dynamic video_generate schema and from xai_video_edit / xai_video_extend; a `model` smuggled into the call is ignored and the configured `video_gen.model` (then the provider default) is what reaches the request. Config-side selection (`video_gen.model`, `video_gen.<provider>.model`, `<PROVIDER>_VIDEO_MODEL`) is unchanged, and the xAI plugin's explicit-model branch is no longer reachable from the tool layer. Refs #83080 |
||
|
|
72fccf2b20 |
fix: name host, attempts and request size when connect retries are exhausted
When every pre-stream connect attempt to an endpoint fails, the user only saw "Connection error." repeated per outer retry; the host, the attempt count and the serialized request size lived in agent.log alone. #97548's reporter had an ~829 KB Codex Responses request fail twice before the stream opened while short chats went through, which is the request-size-limit signature, and nothing on screen said so. One buffered diagnostic line now flows through the existing retry-status path (flushed on terminal failure, dropped on recovery) on both the Codex Responses runtime and the Chat Completions stream worker: "Could not open a stream to <host> after N attempts (request X KB); ...". The host comes from the failed request's URL (the endpoint actually contacted, proxies included) and the size from the buffered httpx request body. Re-entering the stream call from the outer retry/fallback loop does not add another copy. Part of #97548 |
||
|
|
47ab9adc56 |
docs: document hermes auth add openai-codex --browser and auth.codex_login_flow
Providers page (Codex note), CLI reference, credential-pools command table and the OAuth-over-SSH port table, so the fixed :1455 listener and its device-code fallback are discoverable where users look for Codex login help. |
||
|
|
54c01bc19a | chore: merge origin/main (resolve hermes_cli/runtime_provider.py, website/docs/user-guide/features/codex-app-server-runtime.md) | ||
|
|
6c3ff1d732 |
docs(site): docs and generated skill pages stop suggesting /tmp
Hand-written docs and the generated per-skill mirror pages now show the same scratch locations the skills and prompts do (~/.hermes/cache/scratch, $TMPDIR, $HOME/.hermes/cache/scratch/<throwaway-home>) instead of /tmp, and examples that only needed a placeholder use /path/to/... The mirror pages were updated in place rather than regenerated: regenerating from the current sources produces a 200-file unrelated diff (Windows backslash paths, removed skills). Literals that describe /tmp itself stay and carry a no-tmp marker: the sandbox tmpfs configuration, the disk-cleanup plugin's scope, the WSL feature list, the terminal.temp_dir rationale, the Nix container's writable layer and the Docker Compose in-container pulse-cookie path. One tree-listing line in nix-setup.md stays unmarked (a marker would render inside the code block). |
||
|
|
b682a98ab8 |
test(codex): pin proxy override on rotation and model.base_url; document HERMES_CODEX_BASE_URL
Two invariant tests (red on origin/main): a 401 rotation onto a Codex pool row keeps the HERMES_CODEX_BASE_URL target, and model.base_url under model.provider: openai-codex resolves for pool credentials. Adds the previously undocumented HERMES_CODEX_BASE_URL row to the environment variables reference so proxy users can find the knob and its reach. |
||
|
|
85b2a3df6c |
feat(cli): hermes usage [--json] prints the /usage account limits without a session
Codex 5h/weekly windows (and Anthropic/OpenRouter limits) were only reachable through the interactive `/usage` slash command, so cron jobs and shell scripts had no way to read quota state (#33094, #57476). `hermes usage` fetches the same snapshot through `agent.account_usage.fetch_account_usage` — the credential resolution a session with no live agent uses — and prints it with the same renderer; `--json` emits one stable, documented document, exit 1 with a single stderr line when no credential is configured or the fetch fails. Slim redo of #81819 (@himanusia): top-level command instead of `hermes auth usage`, no --all/--account/--reset (the per-entry paths rendered the wrong account for anthropic and the default path bypassed the runtime resolver). Co-authored-by: himanusia <himanusia@users.noreply.github.com> |
||
|
|
03d40f8939 |
fix(gateway): a --global /model keeps the session override under a channel_overrides model; report a failed stale-override cleanup
Precedence is session /model > channel_overrides > config.yaml, so dropping the session override after a --global write regressed chats whose channel_overrides names a model: the confirmation said "switched to gpt-5.5" while the next turn resolved 'channel-model'. The cleanup now runs only when no channel_overrides entry applies to the source; otherwise the override stays (and is written through) so the confirmation stays true. A failed set_model_override(key, None) was logger.debug only while the reply claimed a clean save and memory had already popped the override — the durable stale copy would shadow config.yaml on the next restart (the original #100314 symptom). The cleanup failure is now returned like the config-write error: the in-memory override is kept, the confirmation carries a warning line instead of "Saved to config.yaml", and a riding --reasoning stays session-scoped. Tests (tests/gateway/test_model_picker_persist.py, both red on the previous head): the channel_overrides case drives _handle_model_command with a real JSONL SessionStore and asserts _resolve_session_agent_runtime(source) yields gpt-5.5; the cleanup-failure case asserts the warning and the kept in-memory override. Docs note the channel exception and that the CLI/TUI keep their per-session pin by design (resume restores the model that chat used). |
||
|
|
1a59519244 |
fix(gateway): a --global /model pick leaves config.yaml as the only durable model authority
A `/model <m> --global` switch (typed or picker) used to persist twice: the profile config.yaml AND a per-session `model_override` in the session store. The session copy has higher precedence on rehydration, so after a later global change (CLI `hermes model`, another chat's `--global`) and a gateway restart the stale override silently won — #100314 saw an explicit `gpt-5.6-sol-900k` resume as the base 272K `gpt-5.6-sol`. Now `_record_model_switch` writes config.yaml FIRST and, on success, drops the redundant session override from memory and the store. If the config write fails the switch stays a truthful session override and the confirmation says "config.yaml not updated (...)" plus the session-only hint instead of claiming "Saved to config.yaml"; a riding `--reasoning` pin follows the same effective scope. `--session` and `--once` semantics are unchanged. Tests: two invariants in tests/gateway/test_model_picker_persist.py (typed+picker clear the durable override and a fresh SessionStore rehydrates nothing; failed config write keeps the override and an honest reply), red on base. test_model_command_request_overrides now points get_hermes_home at its own config so the --provider switch resolves session-scoped as intended instead of the sandbox's fresh-install first-pick rule. Fixes #100314 Supersedes #99825 (slim redo; the original wrapped the commit boundary through a sys.modules-swapped mixin). Co-authored-by: Andrex Ibiza, MBA <andrexibiza@gmail.com> |
||
|
|
393ffff04f |
fix(providers): resolve the chatgpt alias in the /model parser and hermes auth login too
The static catalog alias table (models_catalog_static._PROVIDER_ALIASES, consumed by
parse_model_input / normalize_provider in hermes_cli/models.py) had no entry, so
`/model chatgpt:<model>` and `-m chatgpt:<model>` kept the prefix as part of the model
name while `openai-codex:<model>` split correctly. hermes auth login now falls back to
the shared auth alias table instead of its own 6-entry list (custom providers still win).
Pins providers.normalize_provider('chatgpt') as well and documents the aliases in the
--provider row.
Part of #95794
|
||
|
|
99a6ed741a |
Merge pull request #115757 from NousResearch/fix/boa-codex-app-server-lifecycle-migration-mcp
fix(codex-runtime): same-name MCP servers no longer make ~/.codex/config.toml unloadable; adds `hermes codex-runtime migrate` (#79023, salvage #79182) |
||
|
|
1536e1d242 |
docs(discord): finish the free-response auto-thread surfaces
Adds the key to cli-config.yaml.example and the multi-profile per-key list, records the env-over-YAML precedence, and drops the duplicated no_thread_channels clause in the new section. |
||
|
|
774e070731 |
feat(discord): add free_response_auto_thread opt-in
Free-response channels skip auto-threading by default so the bot replies inline (lightweight chat mode). This prevented users who wanted BOTH mention-free replies AND per-conversation threads from getting either. Add a new opt-in `discord.free_response_auto_thread` (env: `DISCORD_FREE_RESPONSE_AUTO_THREAD`, default false) that, when true, re-enables auto-threading in free-response channels. Voice-linked channels continue to skip auto-thread regardless, and the flag is gated behind the global `DISCORD_AUTO_THREAD=true`. Default behavior is unchanged; all 291 existing discord tests pass. |
||
|
|
8d4abc3ea6 |
fix(mcp): retire the n8n bridge catalog entry (#116048)
Stop offering the third-party bridge for new catalog installs. Existing connections keep their saved transport, credentials, and tool selection; the runtime and configured-server controls do not require a manifest. Update CLI examples and document that catalog reinstall is unavailable. Adding n8n's official server remains separate work. |
||
|
|
8b7caf226f |
feat(codex): named custom providers work with the codex_app_server runtime
`model.openai_runtime: codex_app_server` only ever admitted `openai` / `openai-codex`: a named custom provider (`providers.<name>`) resolves to provider="custom" on the named-custom ladder rung, which never ran the runtime gate, so `/codex-runtime codex_app_server` silently left the main turn on Hermes' chat-completions client. And even when routed, thread/start sent only `cwd`, so codex could not know which of its own providers to use. - `_maybe_apply_codex_app_server_runtime` takes `requested_provider` and admits provider="custom" only when `codex_model_provider_id()` finds a configured `providers.<name>` entry (bare `custom`, ollama/vllm aliases and unknown names have no stable id -> ineligible, unchanged). - The named-custom rung applies the same opt-in the pool rung already does for openai/openai-codex. - `CodexAppServerSession(model=, model_provider=)` -> `thread/start.model` / `.modelProvider` (fields verified against the codex 0.147 app-server schema). `_ensure_codex_session` fills them only for custom agents; codex resolves base_url/env_key from its own `[model_providers.<name>]`, so the Hermes credential never enters the JSON-RPC payload. - `tui_gateway/server.py::_make_agent` forwards `requested_provider` so the Desktop/TUI agent knows the provider id (it otherwise collapses to "custom" and codex would fall back to its default provider). - Docs: matching `[model_providers.<name>]` + `env_key` requirement and the bare-`custom` ineligibility. Ported and trimmed from #75191 by @cosin2077 (aux-loop `allow_codex_app_server` plumbing, `cli-config.yaml.example` block and the integration-test suite dropped: background_review already maps codex_app_server -> codex_responses on main). Fixes #75186 |
||
|
|
d09183d8d3 |
feat(cli): add hermes codex-runtime migrate [--dry-run] [--json]
The managed-block marker and the docs have referred to `hermes codex-runtime migrate` all along, but no such CLI subcommand existed: the only way to run the ~/.codex/config.toml migration outside a chat session was to import the private hermes_cli.codex_runtime_plugin_migration.migrate (#79023). The new subcommand group (hermes_cli/subcommands/codex_runtime.py, registered like the other groups) calls the same migrate() the /codex-runtime slash command uses, on the selected profile home, with --dry-run (no write) and --json (full report incl. preserved_user_servers and errors); exit code 1 when the report has errors. Tests: one invariant for the same-name table (single header, valid TOML, user command kept, report lists the name) and one for the CLI dry-run/json path. Docs: conflict policy + command in the codex runtime guide and CLI reference. |
||
|
|
67757285f6 |
feat(sessions): hermes sessions repair-profiles settles crossed-profile durable state
The per-profile store model (#88734), the parent-inheritance fence (#88381), profile-stamped topic rows (#76423) and profile-prefixed voice keys (#75198) are all forward-only: they put NEW state under the right profile and refuse to widen existing damage, but nothing walks the stores and settles what earlier releases left crossed. #113884 found 246 sessions stranded that way and could only warn. `hermes sessions repair-profiles` scans every profile's state.db plus the gateway's voice-mode and sessions.json files and names six kinds of crossing: 1. `profile_name` disagreeing with the row's own session key -> relabel; 2. rows physically in another profile's store -> move (all message generations, usage rows, system prompt) to the owning store, parents before children so lineage survives, copy-then-delete so a crash leaves a duplicate the next run settles; 3. `parent_session_id` crossing namespaces -> sever (own identity kept); 4. routing rows outside the default store under multiplexing -> move (an existing row wins); routing rows for a profile that no longer exists -> drop; 5. Telegram topic bindings and voice-mode entries missing their bot's profile -> relabel from the sessions that hold the chat (ambiguous chats reported); 6. sessions.json mirror entries for an unclaimed namespace -> drop (the legacy import re-injects them into routing every boot). Report-only by default. `--apply` refuses while a gateway owns any store, takes a quick snapshot of every store first, and is idempotent. Two cases are reported but never guessed: rows keyed to a profile that does not exist, and `agent:main` rows inside a named profile's store (`--legacy-main rekey|move` says which of the two histories they are). Storage side lives in `hermes_state_profile_repair.py` (SessionDB mixin); orchestration across stores in `hermes_cli/sessions_repair_profiles.py`; the CLI face in `hermes_cli/sessions_cmd_repair_profiles.py` (pre-DB handler: it opens every store itself). Part of #88715 (PR-6). Closes the remediation gap #113884 only warns about. |
||
|
|
5e95050608 |
fix(agent): a server context rejection the transcript cannot explain is no longer "conversation too long"
A single-slot local server (LM Studio, Ollama) returns 500 "Context size has been exceeded."
when ANOTHER request — a background review from an earlier session — holds its context.
The foreground loop classified that as context_overflow, tried to compress a one-sentence
conversation, could not shrink it, and rendered "This conversation has grown too long …
/new … /compress" with compression_exhausted=True (gateway auto-reset, user message dropped
from the transcript).
_recover_context_length now measures first: when the server quoted no count of its own and
the local request estimate (+ output reservation) sits under half the known window, the turn
ends with distinct copy naming the likely cause (another request on the server / smaller
server window), failure_reason=server_error, retryable, no compression_exhausted — so CLI,
TUI/Desktop and the gateway all render a transient failure. Servers that quote their own
measurement ("233153 tokens > 200000 maximum") and requests near the window keep the
compress-and-retry path unchanged. The two buffered "keeping context_length … and
compressing" notices drop the trailing clause so they read true on both paths.
Fixes #114644
|
||
|
|
1250a3e4eb |
fix(backup): an incomplete archive never prunes the last complete ones
Gate review: `--keep` pruning ran after the summary regardless of `errors`, so a timer hitting the same unreadable file every run would exit 1 each time and still rotate every complete `hermes-backup-*.zip` out after N runs, leaving only incomplete archives. Prune only after a complete backup; the test pins a pre-existing good archive surviving an incomplete run with `--keep 1`. The summary no longer hard-codes the caller's exit code. |
||
|
|
ccb3d968ce |
fix(backup): exit non-zero when a full backup is incomplete
`hermes backup` recorded per-file failures, printed `Backup incomplete: <path>` and still returned shell status 0, so a cron job or systemd timer would publish "successful" archives missing state.db indefinitely. `run_backup()` now returns whether the archive is complete and `cmd_backup()` maps False to exit status 1. The zip is kept so the operator can still restore the rest; hard failures keep their SystemExit(1)/(2). `--quick` is unchanged. Slim redo of #101096 on current main (the branch predates the run_backup / _run_backup_locked split and the backup lock); same policy, same exit codes. Supersedes #68866 (@jbryce) which proposed the policy first. |
||
|
|
8925c70a1c |
docs(whatsapp): group access section says what the gateway admits; env reference rows; trim bridge tests
Groups: policy, group-JID allowlist, and that participants are still authorised by the gateway sender allowlist or pairing (`open` alone admits nobody without one); `require_mention` defaults to false; WHATSAPP_GROUP_POLICY / WHATSAPP_GROUP_ALLOWED_USERS rows in the environment reference. The alt-id node tests collapse to one (the participantAlt case duplicated the first-contact case; the "still resolves via mapping files" case only re-asserted matchesAllowedUser). |