380 Commits
Author SHA1 Message Date
BearHuddlestonandJan-Stefan Janetzky 74915d7af8 docs(memory): name the prune and the plugin-engine limit under checkpoint_required
memory-provider-plugin.md and cli-config.yaml.example say the gate binds to
every compaction authority. This branch adds the proactive tool-result prune
to the authorities it withholds, so both lists name it now (wording from
7e79a8b32b on #149265). They also state what the gate doesn't cover: a
plugin context engine's compress() is checkpointed, because the host calls
on_pre_compress() first, but an engine that removes or rewrites host-
transcript messages outside compress() is neither checkpointed nor refused
yet. The refusal for that engine stays with the plugin-contract work.

The limit sentence is @GottZ's, from his comment on #149265.

Co-authored-by: Jan-Stefan Janetzky <git@gottz.de>
2026-11-05 19:55:33 -06:00
Shenrui Ma 961e275288 docs(stt): clarify language inheritance and auto-detection
Explain why a blank provider language can still resolve to English. Align the guides and environment reference with first-nonempty resolution while preserving configuration values and documenting command-backend limits.

Constraint: Documentation must describe current runtime behavior, not pending default-language or CLI changes.
Confidence: high
Scope-risk: narrow
Reversibility: clean
Tested: Four configuration cases through the real loader and resolver; parsed YAML unchanged.
Tested: Clean English Docusaurus build and rendered table/cross-link checks; git diff --check.
Not-tested: Other documentation locales; no runtime code changed.
2026-11-04 23:16:10 -08:00
Teknium c10474fbb6 docs(stt): blank per-provider language inherits stt.language, not auto-detect
The per-provider `language: ""` comments in config_defaults.py,
cli-config.yaml.example and the voice-mode docs said blank means
auto-detect. Since bc997a36a8 made stt.language default to "en", a
blank provider value inherits that, so users following the comment got
English transcripts of non-English audio (#120100). Correct the text
to the real resolution order; no behaviour change.

Reported-by: Baophan00 (PR #120130)
2026-11-04 21:38:25 -08:00
unsupportedpastelsandFroRaut 60f5029394 feat: Daybreak switch for catalog-listed ChatGPT subscription models
OpenAI Daybreak (Trusted Access for Cyber) reduces cyber refusals for
approved accounts. Codex's model picker has a Daybreak toggle; Hermes had
no way to request it.

The signed-in account's Codex /models catalog already lists, per model,
which cyber programs that account may use
(available_access_programs.cyber). Hermes reads that catalog for Fast and
Ultrafast; it now keeps the cyber list from the same fetch.

- agent.daybreak (default false) is one profile-wide switch, like Codex's.
- Desktop shows a Daybreak switch in a model's options submenu, next to
  Fast, only when this account's catalog lists daybreak_blue for that
  model. Unknown or unreachable catalog: hidden.
- Each Responses request adds access_programs.cyber=daybreak_blue only
  when the switch is on AND the request's credential and model are listed.
  Checked per request, so a /model switch, credential rotation or expired
  catalog never carries it to a model or account without it.
- The gateway re-reads the switch every turn, so a flip applies to the
  next turn of a running chat without rebuilding its agent.
- The opt-in codex app-server runtime does not send it, so the switch is
  hidden there.

Co-authored-by: FroRaut <79215537+Froraut@users.noreply.github.com>
2026-11-02 15:06:27 -05:00
Teknium 5351ed71d4 fix(mem): cap malloc arenas before any server thread exists; honour .env MALLOC_ARENA_MAX
Review fixes for the arena cap:
- serve/dashboard/gateway apply runtime.malloc_arena_max at hermes_cli.main import, right
  after the profile .env and config load and before logging/plugins start any thread
  (glibc latches its arena limit once threads allocate; the old serve call ran after
  plugin discovery). Legacy python -m gateway.run and cli.py --gateway apply it first too.
- MALLOC_ARENA_MAX / glibc.malloc.arena_max present only because Hermes loaded the
  profile .env (absent from /proc/self/environ) is applied via mallopt with the
  operator's number; one present at exec is already glibc's and wins.
- exact tunable-key match, C-int range check, glibc capability gate (gnu_get_libc_version
  + mallopt) instead of sys.platform; tests gate on glibc and drive the real entry point.
2026-11-01 21:54:15 -08:00
Teknium fcbf94c2e7 perf(mem): cap glibc malloc arenas in the gateway and serve processes
glibc gives each new thread its own malloc arena, up to 8 x CPU cores, and keeps
every arena's freed pages. A long-lived gateway churns through ~1,100 short-lived
Python threads an hour (tool heartbeats, turn watchdogs, stream monitors, pipe
drains) on top of native pools (the relay plugin's tokio runtime, anydoc's rayon
pool), so it fills every slot: 157 of 160 arena heaps on a 20-core host after 10 h.

runtime.malloc_arena_max (default 4) sets mallopt(M_ARENA_MAX) in-process at
gateway bootstrap and web_server.start_server (serve / dashboard), next to the
existing RLIMIT_NOFILE floor. In-process rather than MALLOC_ARENA_MAX in the
unit/plist/spawn env, so commands the server runs (terminal tools, MCP servers,
builds) keep glibc's default, and every launch path (systemd, launchd, Desktop,
detached auto-start, docker) is covered by one call. An operator's own
MALLOC_ARENA_MAX / glibc.malloc.arena_max tunable wins; 0/false/null keeps
glibc's default; non-glibc hosts are untouched.
2026-11-01 21:54:15 -08:00
Nico Suarez 33cf20374d feat(gateway): allow intentional silence per room 2026-10-31 15:32:42 +05:30
Victor Kyriazakos a8b987aaa1 fix(bedrock): apply configured request timeouts to Bedrock clients
providers.bedrock.request_timeout_seconds and
providers.bedrock.models.<model>.timeout_seconds resolved correctly through
hermes_cli.timeouts.get_provider_request_timeout, but no Bedrock client was
built with the result. The cached boto3 bedrock-runtime client (ambient and
profile-scoped) was created with no botocore Config, so it kept botocore's
60s read / 60s connect, and the AnthropicBedrock SDK client was built with
_client_timeout(None), i.e. a fixed 900s read. The resolver (3143d32330)
landed after the cached Converse client (0cb8c51fa5) and was wired only into
the OpenAI-wire and native Anthropic builders; 611657487f documented Bedrock
as not covered instead of wiring it.

- _get_bedrock_runtime_client(region, model) resolves the model's timeout
  (model over provider) and passes Config(read_timeout=..., connect_timeout=60)
  only when one is configured; unset or invalid builds the exact
  no-Config client used before.
- Both client caches (ambient and per-profile-home) key on the effective read
  timeout, so models with different overrides in one region never share a
  client, and the same timeout reuses one client. Stale-connection eviction
  drops every timeout slot of the region.
- converse/converse_stream dispatch picks the client from the request's
  modelId, so a /model switch or a fallback to another Bedrock model uses
  that model's timeout. call_converse and the context probe pass their model.
- AnthropicBedrock: build_anthropic_bedrock_client takes the timeout; the
  bound client and the per-request client key carry the active model's
  value (connect stays 10s, unset stays 900s).
- Docs: replace "not currently wired for AWS Bedrock" with what is wired.

This closes a configuration gap only. A socket read timeout bounds each read,
not the whole call; retry policy and the stale detector are unchanged.
2026-10-28 19:51:15 -07:00
Teknium 62f79a434d docs(security): allow_private_urls guards cloud browsers and web tools, not the local sidecar
security.allow_private_urls: false refuses private URLs for web_extract, vision/media
fetches and cloud/CDP browsers, but browser_exec under a cloud provider routes them to
a local Chromium sidecar (browser.auto_local_for_private_urls, default true), and the
plain local Chromium with a local terminal skips the guard by design. Say so in the
security and browser docs, configuration reference, DEFAULT_CONFIG comments and
cli-config.yaml.example, and name auto_local_for_private_urls: false as the way to
refuse. Also correct browser.md's claim that redirects to private hosts are blocked:
routing is decided from URL literals, and a redirect stays on the cloud browser.
2026-10-28 19:07:59 -07:00
Teknium 06662ba995 fix(config): stop seeding and flagging plugin-platform toolset rows (teams, google_chat)
cli-config.yaml.example seeded `teams: [hermes-teams]` and
`google_chat: [hermes-google_chat]` into every install (install.sh,
install.ps1, the Docker stage2 hook, `hermes doctor --fix`). Both are
bundled plugin platforms with no static toolset, so every schema build
(the kanban gate resolves every saved row) logged "platform 'teams' has
no valid toolsets configured" to errors.log on homes that never set up
Teams or Google Chat, and still did once Teams was enabled.

- Drop the two rows from the example: a plugin platform falls back to
  hermes-<platform> on its own (resolution identical with and without
  the row, before and after plugin discovery).
- The runtime warning and `hermes config check` count a platform's own
  hermes-<platform> bundle as valid, so existing homes (incl. the
  homeassistant row pre-left-core homes carry) stay quiet; a typo in the
  row still warns. The own-bundle check runs before the resolver, so the
  hot path does no manifest scan, and the catalog scan from the salvaged
  commit is dropped (862 catalog YAMLs parsed per call).

Found on Hermes Cloud staging.
2026-10-28 13:10:38 -07:00
Teknium 41010b6fef feat(hosted-rooms): read the Discussion budgets through the effective loader, keep defaults on a typo
Layered on the salvaged budget commit: the limits are read with
load_user_config_effective (the gateway-runtime loader: user file + managed overlay +
${VAR} expansion) inside the room owner's scope, and a malformed value is logged and
falls back to today's 3 rounds / 10 replies instead of failing the service construction,
which would take every room on that store offline. Docs say so.

Refs #144648 (WA-21)
Refs #145184
2026-10-27 20:24:05 -07:00
Nixon afe3a9111f feat(hosted-rooms): make discussion budgets configurable 2026-10-27 20:24:05 -07:00
Teknium ee3bcbbbc1 docs: GitHub Copilot is the github-copilot labs plugin; document the provider seams
Provider pages (en + zh-Hans) replace the built-in Copilot sections, table
rows and env rows with a pointer: `hermes plugins install github-copilot`
(labs: built by Nous Research, not supported), auto-installed for profiles
that already use copilot/copilot-acp, sign-in with `hermes auth add copilot`
or COPILOT_GITHUB_TOKEN/GH_TOKEN/GITHUB_TOKEN (gh and Copilot CLI logins are
no longer read). The model-provider plugin guide gets one table of every
ProviderProfile field and hook in providers/base.py, and its examples point at
the labs plugin. The hermes-agent skill references and the
cli-config.yaml.example provider list drop the built-in Copilot rows.
2026-10-25 20:47:11 -07:00
unsupportedpastels 96168a2fa8 fix(gateway): honour legacy reap opt-outs; startup idle floor for orphan ends
Addresses maintainer review on #143626.

Refs #143547
2026-10-25 11:41:09 -04:00
unsupportedpastels b1e7bd0b7c fix(gateway): end local sessions left with no viewer
A CLI, TUI, Desktop or oneshot session whose last viewer vanished without
closing (a killed terminal, a dropped socket, a gateway stop) was never ended,
and every boot loaded every local receipt ever written. The gateway now arms a
grace timer (sessions.orphan_end_grace_s, default 300 s, 0 disables) when the
last viewer detaches without an end reason, and at boot for idle sessions the
previous owner left open. On fire it installs the existing persisted pending
close, so the row ends only after the running turn settles; the turn and its
async delegations are never stopped and an unknown turn waits for Discard.
Boot restores only open rows, unsettled input or stored closes; the rest load
on first touch. Ended sessions stay resumable.

Rows LC-35, LC-36, LC-37.

Refs #143547
2026-10-25 11:41:09 -04:00
Teknium 0f8bbed1ed fix(config): drop browser.extension_control (default and v52 migration)
Nothing reads the key since browser-extension control was removed; the existing v52 step (a
legacy-key step, so unstamped configs get it too) now strips it from user config.
2026-10-23 01:42:29 -07:00
Teknium a9f890f4a6 feat(config): v52 drops browser.backend and moves AGENT_BROWSER_* settings to browser.* keys
DEFAULT_CONFIG gains browser.executable_path, user_data_dir and chromium_args;
backend, snapshot_threshold, record_sessions and the evaluate guardrails leave
(REMOVAL NOTE). browser.camofox becomes an empty free-form mapping: the camofox
catalog plugin reads it, set/get keep accepting its keys, and user values are
never stripped. command_timeout now bounds the Chromium launch.

The v52 step (a legacy-key step, so unversioned configs get it too) deletes
browser.backend, with a one-time notice for off, and copies AGENT_BROWSER_* from
the home's own dotenv layers (plus the process env for the launch home only)
into browser.* keys still unset. A path inside pm's store is not copied: pm and
the Docker image exported their own Chromium that way, and pinning a versioned
store path would break at the next Chromium update. CAMOFOX_* and
AGENT_BROWSER_ENGINE leave OPTIONAL_ENV_VARS (the LEFT_CORE row keeps CAMOFOX_*
stripped from children).
2026-10-23 01:41:25 -07:00
Teknium 9799efea71 docs(delegation): is_available() and the config example describe fail-closed backends
Greptile P2 on #142563: the provider table and cli-config.yaml.example still
promised a fallback to default; also name advanced-delegation, route
fallback_providers inheritance and ctx.delegation_backends.
2026-10-22 14:37:10 -07:00
Tekniumandunsupportedpastels 1266b3a86f docs(delegation): document delegation.backend, delegation.backends and route fields
cli-config.yaml.example and the configuration reference now cover
delegation.backend and delegation.backends.<name> (model_map,
profile_routes); the user and plugin guides cover the settings block,
service_tier and reasoning_effort route checks, the per-result route
report, and that listing a profile in delegation.backends.profile_routes
lets this profile's subagents use that profile's credentials, credential
pool, SOUL.md and terminal policy.

Co-authored-by: unsupportedpastels <274530371+unsupportedpastels@users.noreply.github.com>
2026-10-22 14:37:10 -07:00
Teknium d99af4187b refactor: move the Singularity terminal backend out of core into the singularity catalog plugin
The Singularity/Apptainer backend (tools/environments/singularity.py) now ships
as the official 'singularity' plugin in NousResearch/hermes-official-plugins,
registered through the terminal-environment provider seam under the same
backend name and config keys (terminal.backend: singularity,
terminal.singularity_image / TERMINAL_SINGULARITY_IMAGE, terminal.container_*).

- 'singularity' leaves BUILTIN_BACKEND_NAMES so the plugin can register it;
  every hardcoded classification set drops it (the provider declares
  is_remote/is_container/supports_streams instead).
- LEFT_CORE gains a singularity row: homes whose terminal.backend (or .env
  TERMINAL_ENV) is singularity get the catalog plugin once, on hermes update
  or agent/gateway start. An unknown-backend error for 'singularity' names
  the install command (new row field terminal_backend).
- A plugin backend receives its per-task '<backend>_image' override; batch
  runs register one for the active backend.
- BaseEnvironment's sudo-probe knob is renamed _passwordless_sudo_probe_supported:
  plugin backends set it, and the old spelling trips the install scanner.
- Core's scratch-dir disk warning and process-exit sweep were Singularity-only
  and move to the plugin; the singularity setup row, dashboard probe/row and
  config defaults/bridges go too. Docs point at the plugin.
2026-10-22 05:26:10 -07:00
emozilla 1cd4b29eb1 Merge main into feat/mcp-events-webhook
Resolve gateway/platforms/webhook.py: main now derives delivery_id from the
request headers before it parses the body, so the MCP event route overrides
it with mcp-event:<eventId> right after the control-envelope hook.
2026-10-20 23:37:50 -04:00
emozilla e6548b932c fix(mcp): rename the list_events utility to list_event_types
A server's own tool shadows a utility of the same name, and list_events is a
common tool on calendar servers. On those servers the model had no way to list
event types.
2026-10-20 23:36:32 -04:00
Teknium ab1c29d8bb fix: worktree sweep keeps any tree holding work an archive cannot restore
The second review found trees deleted with bytes or commits that were in
neither git nor the archive: untracked files over archive_max_file_mb
(skipped from the snapshot, tree still removed), gitignored embedded
repositories (ls-files --exclude-standard never lists them) and every
ignored file (.env edits, *.secret, info/exclude notes). The classic
pruner had three older gaps of the same class: status.showUntrackedFiles=no
made new files invisible, skip-worktree/assume-unchanged edits read clean,
and commits held only by refs/worktree/* or refs/bisect/* died with the
tree's admin dir.

Rule now enforced at one decision point (worktree_retention.removal_blocker)
used by archive_preflight, the post-snapshot removal boundary in
archive_then_remove (sweep, exit Archive, subagent finalize), the classic
clean-tree pruner, the attended `hermes worktree prune` and the clean exit
path: a tree is removed only when everything irreproducible is in git or the
archive. Kept: keep-large-untracked (archive ref stays), keep-unarchivable
(embedded repo, ignored or not), keep-worktree-refs, keep-ignored-work
(ignored files outside a regenerable-dir allowlist that are not
byte-identical to the main checkout's copy, so .worktreeinclude .env copies
never block). Exit Archive and the subagent note name the reason. Every
dirty check forces --untracked-files=normal and counts flagged-index edits.
The ignored-path walk skips dependency stores without descending and is
capped per tree (unverifiable -> keep).
2026-10-17 21:49:14 -07:00
Teknium 3882113085 feat: archive stale dirty worktrees before removing them, sweep every repo, ask on dirty exit
The worktree pruner never removed a dirty or unpushed tree at any age, so leftovers of finished
lanes (stray probe files, WIP commits on closed PRs) accumulated forever: 658 trees / ~1 TB on
one machine, 553 of them named lane trees. Approved design (Teknium, 2026-10-17):

- Snapshot-then-delete: an idle tree past its tier (hermes-*/subagent-* 24h, named 72h) with
  uncommitted changes or unpushed commits is archived to refs/hermes/archive/<tree> (HEAD +
  staged + unstaged + non-ignored untracked bytes, built with a temporary index; large untracked
  files skipped and listed) and then removed. A failed archive keeps the tree. Branch kept when
  published on a remote, otherwise the archive anchors its commits.
- Never touched at any age or budget: live hermes lock owners, any live process whose cwd/open fd
  is inside the tree (/proc on Linux, psutil cwd elsewhere), trees with recent file/ref activity,
  Kanban t_<hex> trees.
- No count cap; optional worktrees.max_total_gb evicts the oldest idle trees first.
- Per-profile registry (<HERMES_HOME>/worktree_repos.json) of every repo Hermes made a worktree
  in (hermes -w, /worktree new, subagent isolation, Desktop git pane); the 6h cron sweep unions
  it with its existing sources, scoped and throttled per profile.
- Subagent worktrees with commits/edits are archived and removed at child finish (branch kept).
- Interactive `hermes -w` (CLI and --tui) exit with a dirty/unpushed tree asks Keep (default) /
  Archive & remove / Remove and discard; non-interactive exits never ask.
- `hermes worktree restore <name> [--path]`, archives in `hermes worktree list`, a full
  decision table in `hermes worktree prune --dry-run`, archive retention (30 days).
- worktrees.archive_dirty: false restores the classic never-delete-dirty pruner exactly.
2026-10-17 21:49:14 -07:00
Teknium 33a25058b2 fix(process): stop re-waiting on a quiet background process; notify on exit instead
Models re-issue byte-identical process_manage wait/poll calls on a background
process that has printed nothing new: in 30 days of one heavy user's sessions,
~1,900 of 2,235 waits repeated the previous call verbatim, each a full model
turn (~231k prompt tokens on a frontier model), often right after the timeout
note said the process was still running.

After the second consecutive wait/poll on a running session with no new
output, Hermes now enables notify_on_complete for that session (stamping
gateway routing and arming the per-process watcher when it was spawned
silently, so messaging chats get the exit too) and the result tells the model
not to wait or poll again: the exit code and final output arrive as their own
message. New output, exit, or any other action on the session resets the count.

Only where a completion can reach the agent: finite turns (chat -q / -z),
cron, Kanban workers, stateless API sessions and delegated subagents keep the
old result. terminal.process_wait_auto_notify: false restores the old
behaviour everywhere.

The counting/policy lives in tools/process_registry_wait_policy.py; the
process_registry facade only routes session-action results through it.
2026-10-17 04:35:05 -07:00
unsupportedpastels 887d87a190 docs(config): say an activated venv reaches terminal through PATH only
The example config said VIRTUAL_ENV reaches a session's terminal tool.
It does not: as in the classic CLI, the terminal's child environment
strips the VIRTUAL_ENV/CONDA_PREFIX markers, so the client's venv
reaches it through PATH (which python), while execute_code sees the
markers themselves.

Refs #134889 (T3)
Refs #139523
2026-10-16 17:49:54 -04:00
unsupportedpastels 421a6f5cfd fix(config): register terminal.client_env_passthrough with an empty default
The gateway read terminal.client_env_passthrough, but the config defaults
didn't declare it. It is now in the terminal defaults as an empty list,
shown in cli-config.yaml.example and the configuration guide, and a
user's list survives load and save.

Refs #134889 (T3)
Refs #139523
2026-10-16 17:49:54 -04:00
unsupportedpastels 027e62ed17 fix(plugins): say plainly that the gateway does not load project plugins
On main each `hermes` process scanned ./.hermes/plugins in its own cwd
when HERMES_ENABLE_PROJECT_PLUGINS was set. Every session now runs in the
one gateway, which discovers plugins once from its own directory, so a
project's plugins silently never load.

The plugins guide now says so in a Project-local plugins section. When a
CLI or TUI session starts with a forwarded HERMES_ENABLE_PROJECT_PLUGINS
in a folder whose ./.hermes/plugins has entries, the gateway logs a line
to gateway.log and session.info carries it as config_warning: the TUI
already renders that at session start, and the classic CLI now prints it.
A folder that can't be listed skips the notice; it never fails
session.create or session.info. Nothing is loaded; loading project
plugins in a plugin host is left for later.

Host-specific client values also need proof that the client is on this
host. A connection is native_owner only when it redeemed a ticket minted
over the gateway's private control socket, from loopback with no Origin
header; session.create passes that as client_local, and a client can't
send it (an unknown field). Without it only HERMES_EPHEMERAL_SYSTEM_PROMPT
and HERMES_INFERENCE_PROVIDER from client_env apply: the session keeps the
gateway's toolchain, prefill file and project-plugins check
(client_env_remote on its policy, left out of the policy identity when
false), and its config_warning says the client's environment was not
applied because the client is not attached to this gateway locally.

Refs #134889 (LC-4)
Refs #134889 (LC-8)
Refs #134889 (T3)
Refs #139523
2026-10-16 17:49:54 -04:00
unsupportedpastels 979c08e35d fix(tools): run a CLI/TUI session's tools with its client's toolchain environment
On main the terminal tool, its background processes and execute_code ran
with the launching shell's environment: an activated venv, nvm/pyenv
shims, PATH. Under the one gateway they ran with the daemon's
environment, so `python`, `node` and friends resolved to whatever the
gateway was started with.

The allowlisted toolchain part of a session's client_env (PATH,
VIRTUAL_ENV, CONDA_*/NODE_*/NVM_*/PYENV_*, GOPATH, GOROOT, CARGO_HOME,
RUSTUP_HOME, JAVA_HOME, plus names a profile lists in the new
terminal.client_env_passthrough) now replaces the daemon's values for that
session's local terminal commands, background processes and execute_code
children only, including execute_code's project-mode interpreter choice.
PATH stays the gateway's when the client sent none. The backend's own
terminal.env still wins. No full shell snapshot is taken. Messaging, cron
and Desktop sessions keep the gateway's environment.

Credential-looking names are never sent, stored or bound: one predicate,
is_secret_env_name, now shared with execute_code's secret policy, drops
them on the client, before the policy is stored and again before binding
(secrets keep using terminal.env_passthrough). execute_code applies its
whole scrub, including the delegated-child restrictions, after merging the
client's toolchain, and its list now also strips *COOKIE* names.

Refs #134889 (T3)
Refs #139523
2026-10-16 17:49:54 -04:00
Teknium 6899e45dec fix(agent): replayed images leave the window in sticky batches
A strict newest-keep window turned the image that fell out back into
[screenshot] text in an old row on every new photo past keep, rewriting
the cached prefix each turn. The replay frontier now advances only in
whole batches of keep (newest keep..2*keep-1 images replay), counted by
slot position so a missing/changed file never pulls an older image back.
Earlier rows change at most once per keep new images and a dropped image
never returns.
2026-10-16 14:36:03 -07:00
TekniumandAlexbob0 c1b4b62a49 fix(agent): follow-up turns rebuilt from the session DB see the user's image again (#136216)
The session DB stores user rows as text: session_persistence._durable_content turns each image
part into a trailing "[screenshot]" line. Every turn rebuilt from state.db (API-server requests
continuing a session with X-Hermes-Session-Id, hermes chat --resume, gateway restarts) therefore
sent the model that placeholder instead of the photo the user asked about one message earlier.

The bytes were never lost: every native attach path already retains them under the profile's
content-addressed cache/documents/native-inputs/<sha256>/<name> and names that path in the same
row ("[Image attached at: <path>]"). build_api_messages now re-attaches the
vision.replay_recent_images newest images (default 3, 0 = off) on the wire copy when the turn's
model takes images natively, encoding them with the same _file_to_data_url the live turn used, so
the rebuilt row is byte-identical to the original request and the prompt-cache prefix holds.

Only retained native-inputs bytes whose digest still matches are replayed. Remote URLs, missing or
changed files, images outside the window, and a user-typed "[screenshot]" line (no paired attach
hint) keep their text. A non-integer config value warns and disables the replay. No new store,
DB column or retention path.

Co-authored-by: Alexbob0 <190955252+Alexbob0@users.noreply.github.com>
2026-10-16 14:36:03 -07:00
Teknium 968c87f6bc feat(sessions): auto-prune is opt-in again; ask once when the disk runs low
sessions.auto_prune goes back to false by default. Deleting history is the
user's call, but an install that never decides still needs to hear about it
when the disk holding state.db runs low, or state.db grows until the disk
fills (the reason #54189 turned it on).

While the user's config.yaml has no sessions.auto_prune key, every surface
asks once the disk is low (< 2 GiB free, or < 10% free with < 20 GiB left):

- classic CLI: a y / n / Enter(later) question at startup (TTY only)
- TUI and Desktop: one sticky notification.show per profile per process
- gateway: the question on each served profile's home channel, at most
  once per 7-day snooze window
- /autoprune on|off|later|status answers it on every surface (registry
  executor, so the text is identical); on/off writes the explicit key and
  the question never comes back, later snoozes a week

The prompt estimates what a prune would free (read-only count of ended
sessions past retention_days, scaled by their share of messages) and says
plainly when pruning will not fix the disk: no session old enough, or only
a small amount reclaimable.

The hot-path check is os.stat + shutil.disk_usage + one small YAML read;
the DB estimate only runs once that gate fires, off the prompt thread on
TUI/Desktop.

gateway/run.py and tui_gateway stay under their line caps: the media-cache
chore moves to the new gateway/run_housekeeping_disk.py with the prompt
chore.
2026-10-16 13:56:20 -07:00
Claude 01b6651bcf feat(mcp): webhook-mode MCP events client (experimental)
Implements the client side of the MCP events design sketch
(modelcontextprotocol/experimental-ext-triggers-events) for webhook
delivery. A server opted in with `events: true` gets list_events,
subscribe_event and unsubscribe_event utility tools. Each subscription
is a dynamic webhook route with its own whsec_ secret; the webhook
adapter answers the server's signed verification challenge, handles
the terminated and gap envelopes, and dedupes occurrences by eventId.
The MCP server task refreshes every subscription before its granted
lifetime runs out, and again on reconnect.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Abve7vgBZHnb6XXYw4xbvF
2026-10-16 20:03:05 +00:00
Teknium 408cc0d12e Merge origin/main (#32: 36 commits to cf23a1a5cc) into feat/unified-gateway-runtime
17 conflicts, all from main's lint sweep (ruff UP007/UP017/UP022/UP028/UP031/UP037/UP041,
FURB188/FURB192); no other main commit touches a conflicted file. Resolution: the branch side
of each file plus main's exact autofix (ruff 0.15.10, main's ruff.strict.toml, --fix
--unsafe-fixes). ruff.strict.toml takes main's copy. The pool-retirement live fixture stays
deleted with the pooled backend this branch retires.
2026-10-09 10:24:04 -07:00
Erosika 2327290b0d fix(memory): make external prefetch spilling opt-in 2026-10-09 02:59:16 -07:00
Teknium ca38632fc0 Merge origin/main (#17: 454 commits to e6757f2029) into feat/unified-gateway-runtime
12 conflicted files, resolved in three lanes:
- gateway/run.py: take the branch split; main's tirith removal and spool-replay
  ordering ported into run_runtime_init.py / run_bootstrap.py.
- config migrations: main keeps v50 (tirith); the branch's bot-chat timeout removal
  becomes v51 (_config_version 51, template + test updated).
- hermes_state_usage.py: main's voice_chat task route threaded through the branch's
  owner transaction; worker.persist usage allowlist accepts task.
- tui_gateway/server.py: branch side; main's edits applied where the code now lives
  (agent_factory, session_registry). Desktop sessions get the catalog toolset on the
  authority path; daemon-run finite CLI turns are not watched turns; lazy session info
  reports nous/welcome on the free tier.
- Desktop/Ink: one branch-child spinner fix (main's route-token check); main's
  uninstall i18n keys kept, pool keys stay removed; voice_turn forwarded and accepted
  by the authority submit (surface_v1.voice_turn).
- chat_ws / PTY keepalive test: main's stricter test kept.
2026-10-07 06:48:09 -07:00
Teknium 1e500ebce2 Merge origin/main (#16: 4,465 commits to 9dcab1e440) into feat/unified-gateway-runtime
99 conflicted files resolved in nine area lanes (Desktop composer, Desktop renderer/shared
client, Electron main, Ink, tui_gateway, gateway, state, CLI/config, cron/tools/CI). The
unified runtime's architecture wins where it deliberately replaced a mechanism (pool,
fallback lanes, claim poller, renderer queue); every orthogonal main change is kept, ported
into the <stem>_<topic> sibling that now owns relocated code.

Notable ports and repairs in this merge:
- tui_gateway/server.py relocated regions: atomic _attach_built_agent (#49852), slash ranking
  registry names, minted-key helpers methods_session.py calls, request_overrides, service tier.
- gateway/run_turn*.py: i18n accessors for the removed reply constants, first-contact
  internal/DM rule, rotated-compression persist, fast-mode tiers, prompt-pin rehydration.
- webhook: route_from_chat_id decodes main's opaque webhook:v2 chat ids (old ids still read).
- ws.py: main's per-connection dispatch queue with authority-first dispatch inside it.
- state: schema event for runtime revision renumbered to 31; worker mirrors updated for
  main's message uid/merge fields, on_contended lease arg, compression columns.
- config: branch's bot_chat_delivery_timeout removal renumbered v47 -> v50.
- Electron: primary-profile pin, redirect header scoping, remote fetchJsonForBackend, SSH
  teardown on profile delete, held-update dial gate restored (lost in earlier merges).
- Desktop: canonical clarify.request mapped to main's questions[] card; replay truncation vs
  snapshot_required; periodic secondary prune tick restored; DELETE already_absent restored.
- cron/tools: diagnostic category on cron failure notices restored; owner_holds_delivery
  ported onto the authority; admitted receipts can no longer be cancelled locally.
2026-10-06 14:24:24 -07:00
Teknium 911da42e9e refactor(security): remove the bundled tirith scanner from core
tirith ran `tirith check` on every terminal command, default-on with fail-open, as a
pinned PM binary auto-downloaded at startup. Measured against the shipped 0.4.2 binary
it caught 14/16 attack commands but flagged 12/24 everyday agent commands
(`echo $((6*7))`, `"$VAR/x.sh"`, `gh api | python3`, LAN curls, `.dev` hosts,
`pip install` via analysis_incomplete), and in cron/-q deny mode each of those is a
hard block. It only ever saw terminal strings (execute_code, writes and the browser
bypass it), fail-open meant no protection wherever the binary could not install
(Windows, Termux, x64 musl), and it produced a steady issue/PR stream (fail_open
semantics four times, install loops filling disks, a SIGSEGV hang, session-approval
scoping, third-party promo text on approval cards).

Removed: tools/tirith_security.py, the tirith branches of the command guard (the
is_tirith tuple, session-only "always" downgrade, import-error/fail-open paths,
unattended tirith pass), the CLI and gateway startup installers, the PM package and
lock pins, the nix runtime dep, security.tirith_* defaults, the CLI warning string
and three tirith tips (gaps refilled from the tail so translations survive), plus
the gateway's "no automated assessor" warning that only existed to point at tirith.

Config v50 drops security.tirith_* and enables nothing in its place. Hermes ships
no tirith plugin; if tirith upstream publishes one, it goes through normal catalog
admission under their name.
2026-10-06 14:22:13 -07:00
Julien Talbot 673611161e docs(stt): clarify xAI model default
(cherry picked from commit 30b38e713c4b5b736f41ecb7bf8bd084e637a4d0)
2026-10-06 12:08:49 -07:00
Julien Talbot 56b0f3cb55 feat(stt): pin xAI transcription to grok-voice-transcribe-2.0
POST /v1/stt still defaults to grok-voice-transcribe-1.0 when model is
omitted. Hermes logged "grok-stt" and never sent the field, so voice notes
stayed on 1.0. Send stt.xai.model (default 2.0, overridable via STT_XAI_MODEL)
on the gateway and the desktop client-direct path.

(cherry picked from commit c869631946e9a9d84b1738bfdd278f926300da00)
2026-10-06 12:08:49 -07:00
nikkoxgonzales ffe73c37f0 fix(mcp): make sampling.tools capability opt-in via expose_client_tools 2026-10-05 16:47:32 -05:00
Brooklyn Nicholsonandembwl0x 4f91c5a462 fix(stt): opt-in 16 kHz mono normalization for command STT providers
Command STT providers received the raw audio file with no format
normalization: _transcribe_command_stt only checked .exists() before
substituting {input_path}. Desktop voice recordings arrive as WebM/Opus
48 kHz and providers with container/sample-rate contracts (e.g. Tencent
16k_zh) reject the raw webm, so desktop voice chat failed on every
message. _transcode_audio_for_stt (ffmpeg -> 16 kHz mono m4a) existed
but was wired only into the OpenAI path's rejection retry.

Add stt.providers.<name>.normalize (default off, so whisper-style CLIs
that accept any container keep the original file): when truthy, the
input is transcoded into the per-call temp dir and {input_path} points
at the normalized file. Transcode failure returns an error envelope
instead of silently feeding the raw container to a provider the user
declared needs normalization.

Fixes #81811

Co-authored-by: embwl0x <embwl0x@users.noreply.github.com>
2026-10-05 16:40:15 -05:00
kshitijk4poor adc55b6a71 docs(gateway): cron_drain_timeout floor also covers api_server runs
Gate finding B01 #1 (2c + quality): since api_server (/v1) runs now ride
the cron drain floor, the docstrings and comments for
parse_cron_drain_timeout, _load_cron_drain_timeout,
_get_cron_drain_timeout, the DEFAULT_GATEWAY_CRON_DRAIN_TIMEOUT comment,
cli-config.yaml.example and the messaging guide still called it a
cron-only floor. Say it covers /v1 runs too, that 0 opts them out as
well, and why: an interrupted /v1 run fails its waiting caller.
2026-10-05 19:39:09 +05:30
teknium1 fb9fc8a3dd refactor: move Home Assistant out of core into the homeassistant catalog plugin
The Home Assistant gateway platform (plugins/platforms/homeassistant) and the
homeassistant toolset (tools/homeassistant_tool.py) now ship as the official
catalog plugin `homeassistant` (NousResearch/hermes-homeassistant), with the
same platform name, toolset/tool names, config keys and env vars.

Migration (hermes_cli/left_core_migration.py, a generic "left core" table
reusing the memory-provider migration's helpers): every profile home that
used Home Assistant (HASS_TOKEN in its .env, platforms.homeassistant enabled
or holding a token, or the homeassistant toolset selected) gets the plugin
installed from the catalog at its reviewed pin by `hermes update`, and lazily
once per process at agent start / gateway start (honours
security.allow_lazy_installs). Every outcome reaches terminal, Desktop and
chat; a gateway-start outcome is handed to that home's first agent.

Core HA special cases become generic PlatformEntry seams the plugin uses:
trusted_inbound (allowlist bypass for event-bus platforms), display_tier
(built-in display defaults) and shared_env_prefixes (profile-clone ownership
rule for env shared with the plugin's tools). Cron delivery uses the existing
cron_deliver_env_var seam; HASS_TOKEN stays stripped from subprocesses via the
installed plugin's manifest secrets.
2026-10-04 00:08:20 -07:00
teknium1 7e53b3ef82 chore(memory): remove bundled honcho provider; install from the plugin catalog 2026-10-02 04:39:28 -07:00
Superfly73 336b557bec fix(gateway): skip cron runs in their own restart-safe scope in the restart wait
A cron run handed to an external worker in its own transient systemd scope
outlives the gateway: the shutdown drain deliberately leaves it unmarked
(``mark_running_jobs_interrupted``) and its final send rides the durable
delivery queue, drained by whichever gateway is live next. The after-turn
restart wait still counted it as active work, so ``/restart``, SIGUSR1 and
``hermes update`` held the gateway in ``draining`` — refusing every new turn —
for up to ``agent.restart_after_turn_timeout`` (30 min by default) while the
worker kept running regardless. Observed in the field: a 100-minute job turned
a restart into a ~30-minute lockout.

- ``cron.scheduler`` records whether an accepted handoff got its own scope
  (``dispatch.mode == "scoped"``) and exposes
  ``get_restart_safe_external_job_ids()``; runs already reported as wedged are
  excluded there too, because the gateway subtracts both counts and a run in
  both sets would be subtracted twice.
- ``_awaitable_work_count()`` subtracts that count as a second, disjoint
  exclusion; ``_describe_active_work()`` marks the unit (``restart_safe``).
- A degraded worker (external, but no reachable user bus, so it shares the
  gateway cgroup) is NOT listed and is still awaited — the restart would kill
  it mid-run.
- The wait and observation log lines now name both exclusion classes instead of
  claiming everything still listed will be interrupted.

Tests: tests/gateway/test_restart_drain.py (skip + control),
tests/cron/test_restart_safe_worker.py (registry contract).
Docs: updating.md restart section, cli-config.yaml.example.

(cherry picked from commit 6a2f0b77cf07a60510430fc4ef8b75539ee422fb)
2026-10-01 22:25:21 -04:00
kshitijk4poor e929702021 fix(lsp): trust the workspace the session was opened in, not only the process cwd
The trust anchor was os.getcwd(), which is the user's project only for a
plain `hermes` launched inside a repo. `hermes -w` sets TERMINAL_CWD to its
new worktree without a chdir, the Desktop/TUI backend runs from the install
tree or $HOME with the project bound as the session cwd, the gateway runs
from HERMES_HOME, and cron binds a per-job workdir. All of them lost
pyright's project venv, rust-analyzer/gopls/jdtls/... and the .ts/.rs lint
fallbacks inside the user's own repository.

operator_workspace_roots() now adds the worktree of resolve_agent_cwd()
(session cwd, then TERMINAL_CWD) to the launch dir's; only surfaces set
those, the agent's `cd` moves the terminal env's cwd, not them. $HOME is
never an anchor: a dotfiles repo there would trust every directory below it.
The service remembers every anchor it has seen, because the loop thread
that spawns servers has no session context of its own.

- The lint gate checks only the linter's cwd: npx and rustup resolve the
  toolchain from there (subprocess cwd=env.cwd), not from the file's dir.
  The try/except that wrapped code which cannot fail is gone.
- Trust is computed once per spawn and handed to ServerContext.
- Multi-root servers (pyright) share one process across trusted roots
  only; an untrusted root gets its own, so whichever root spawned first no
  longer decides the interpreter for the others.
2026-09-29 03:28:46 +05:30
John Paul Soliva 59ec281140 fix(lsp): deny by default in an untrusted workspace and skip the toolchain lint fallbacks there
Pinning settings per server left every other server free to run project
code: rust-analyzer runs cargo check (build.rs, proc-macros) on each
didSave despite the disabled init options, and jdtls, kotlin-language-server,
elixir-ls, zls, clojure-lsp and haskell-language-server evaluate build
files when they start.

In an untrusted workspace only UNTRUSTED_SAFE_SERVERS now start (pyright,
typescript, vue, svelte with their pinned settings; bash, yaml,
dockerfile, intelephense, clangd without --query-driver). Every other
built-in or user-declared server is skipped at the spawn chokepoint and
in enabled_for, logged once per root and listed by hermes lsp status.
The rust-analyzer init tweak is gone: it never starts untrusted now.

On a local backend the npx tsc and rustfmt --check fallbacks are skipped
the same way, since npx resolves the repo's node_modules/.bin/tsc (or its
.npmrc registry) and rustup honours its rust-toolchain.toml.
2026-09-29 03:28:46 +05:30
John Paul Soliva f5c754d436 fix(lsp): never run a cloned repository's own interpreter or TypeScript SDK in an untrusted workspace
Language servers start with no approval on every write_file/patch inside any
git worktree. Hermes pointed pyright at the checkout's own .venv/venv python
(pyright executes it), let Vue and typescript-language-server load the
checkout's node_modules/typescript, and left svelte-language-server and
rust-analyzer loading project config, build scripts and proc-macros.

A workspace is now trusted only when it is the git worktree Hermes was
launched in or sits under an lsp.trusted_workspaces entry. Elsewhere pyright
keeps VIRTUAL_ENV or the Hermes-managed python, TypeScript/Vue are pinned to
the Hermes-side SDK, svelte gets isTrusted:false and rust-analyzer runs
without build scripts or proc-macros.
2026-09-29 03:28:46 +05:30
Teknium 7c799a6565 feat(vercel): fresh sandboxes use a managed image (universal:latest); runtime presets deprecated, migration 49 (#126741)
* feat(vercel): start fresh sandboxes from a managed image instead of the deprecated runtime

Vercel deprecated Sandbox runtimes (node24/node22/python3.13) in Aug 2026 in favour of
images, and rejects runtime+image together and runtime with a snapshot source. New
terminal.vercel_image (default vercel/sandbox/universal:latest, Node 24 + Python 3.14)
picks the image for fresh sandboxes; a pinned terminal.vercel_runtime still works, wins
over the image and logs a deprecation warning; snapshot restores send neither.

Setup wizard prompts for the image, dashboard exposes both keys, status/config show the
effective choice, TERMINAL_VERCEL_IMAGE bridges config to the tool like its siblings.

* feat(config): migration 49 drops the seeded node24 Vercel runtime pin

Every pre-49 config.yaml carries terminal.vercel_runtime: node24 (the template default) and
the setup wizard mirrored it into .env as TERMINAL_VERCEL_RUNTIME. Both are the default
copied, not a choice, so the migration drops them and fresh sandboxes follow vercel_image;
node22 / python3.13 pins are the user's and survive. Persisted sandboxes are unaffected:
a snapshot restore never sends a runtime or an image.
2026-09-28 12:33:01 -07:00