Adds a first-run Desktop choice between installing Hermes locally and
connecting to an existing remote Hermes gateway. The choice appears after
backend resolution but before ensureRuntime(), so selecting remote cannot
accidentally trigger local bootstrap.
New modules:
- first-run-setup-gate: concurrent first-run decision gate and reset semantics
- primary-backend-startup: Electron-free orchestration seam (saved remote
resolution, gate decision, remote re-resolution, local continuation)
- primary-connection-rehome: prevents dual-owner race where both cold boot()
and renderer softSwitch() could connect simultaneously
- first-run-remote-form: extracted remote form with stale-result guards
Reuses existing connection-config IPC, encrypted token storage, OAuth
session partition, and primary backend resolution.
Fixes#38602Fixes#36970
Follow-up to salvaged PR #70549:
- Replace fragile 'or' assertions with single precise checks that catch
partial-rewrite regressions (would have masked a missing closing brace)
- Add test_pty_path_uses_rewritten_command covering the PTY spawn path
that was modified but previously untested
Base-url+model dedup was meant for custom shim aliases, but it also
skipped first-class providers that share an inference host while using
different credentials. That stranded xai-oauth spending-limit failover
to the xai API-key provider when both used the same model slug.
A session pinned to a named custom provider could silently reroute to the
user's default provider on resume/rebuild. Session rows persist the RESOLVED
provider — bare "custom" for every named providers:/custom_providers: entry —
and when no base_url survived in model_config, the existing heal
(canonical_custom_identity) had only the config.model.provider fallback left.
For users whose global default is a BUILT-IN provider (e.g. nous) that tier
cannot fire, so the bare provider was dropped, resume fell back to the default
provider with the session's custom model name, and the default endpoint 404'd
with "Model '<x>' not found. The requested model does not exist in our
configuration or OpenRouter catalog." Re-selecting via /model fixed it until
the next resume — the reported symptom.
Add a model-name recovery tier between the base_url reverse-lookup and the
config fallback: find_custom_provider_identity_by_model() maps the stored
model back to the entry that serves it (model/default_model/models catalog,
dict and legacy list shapes). The session row always stores the model, so the
entry identity survives even when the row has no base_url AND the global
default points elsewhere.
All five bare-custom heal sites in tui_gateway/server.py now pass the model:
_ensure_session_db_row, _stored_session_runtime_overrides,
_runtime_model_config, _make_agent, and _model_picker_context.
Stale truncate_before_user_ordinal=0 from a desynced Desktop client
resolved to history[:0] and replace_messages() wiped the durable
transcript. Require confirm_empty_truncate for that edge and have
intentional first-turn restore/regenerate paths send it.
The Telegram adapter's connect retry loop could silently stall after
'Connecting to Telegram (attempt 1/8)...' with the event loop permanently
parked in select() — all threads idle, no attempt 2/8 ever scheduled.
Root cause analysis:
- The retry loop reused the same Application object across all
8 attempts. After a failed initialize() the app could be in a partially-
initialized state (closed httpx transports from ,
or flag set before the hang) causing subsequent calls
to silently skip real initialization.
- CancelledError (a BaseException, not an Exception) propagated silently
through all except handlers with no logging — the task driving the retry
loop could exit without any trace.
- No total watchdog bound existed for the entire retry loop; only per-attempt
timeouts via _await_with_thread_deadline. If the loop itself stalled
between attempts (between-attempt sleep, cleanup, or scheduling), there
was no timeout to catch it.
Fixes:
1. **Total watchdog deadline**: Compute a total deadline for the entire
connect loop (8 attempts × init_timeout + 120s margin). Before each
attempt, check the wall clock; if exceeded, raise OSError immediately
instead of attempting another initialize().
2. **Fresh Application per retry**: On each failed attempt, rebuild
via and re-register all handlers. The old
app is best-effort shutdown with . This ensures
each retry starts with a clean slate — no stale transports, no stale
flag, no leaked state from the previous attempt.
3. **BaseException logging + propagation**: Added
(placed LAST after all other handlers) to log CancelledError and other
non-Exception signals before propagating. Previously these exited the
retry loop silently with no log message.
4. ** block for app rebuild**: The clause runs after
every failed attempt that isn't the last, rebuilding the app and
discarding the old one regardless of which exception class caused the
failure.
Issue #68915: when the agent runs a compound command with trailing & (e.g.
`cd /app && node server.js &`), bash parses it as `(A && B) &` — a subshell
that holds the stdout pipe open forever when B is a long-running server.
The existing _rewrite_compound_background in terminal_tool.py correctly
rewrites this to `A && { B & }` to avoid the subshell fork, but it was only
applied in the foreground execute() path (tools/environments/base.py).
The background spawn_local() path bypasses base.py entirely and passed the
raw command directly to Popen/PTY, leaving the deadlock unmitigated.
Fix: apply _rewrite_compound_background in spawn_local() before the command
is passed to Popen or PTY spawn. Uses a lazy import to avoid circular
dependency (terminal_tool imports process_registry).
- PTY spawn path: now uses safe_command (rewritten)
- Popen spawn path: now uses safe_command (rewritten)
- Session.command still stores the original (unrewritten) command for display
- Simple `cmd &` is left unchanged (no subshell bug)
Tests: 4 regression tests verifying (1) compound is rewritten, (2) simple bg
is preserved, (3) multi-line compounds are rewritten, (4) session.command
stores original.
Follow-up on the salvaged #68469 commit:
- Literal-IP hostnames never take the proxy DNS-delegation path (a
getaddrinfo failure on a literal IP is not a proxy-environment
symptom, and IPs need no DNS) — keeps the private-IP/metadata floor
intact under proxy env vars.
- Adds TestProxyEnvironmentDnsDelegation: delegation fires only for
hostnames, metadata hostname/IP floor holds, DNS-success path
unchanged, empty proxy var ignored.
- Guards the three pre-existing DNS-failure tests against ambient
proxy env vars so they don't flake on developer machines.
When the runtime blocks direct DNS (NVIDIA OpenShell, Docker + Squid,
corporate proxy with DNS-only-via-proxy), socket.getaddrinfo() fails
and is_safe_url() blocks *all* requests — including legitimate public
URLs via the configured proxy.
Add _proxy_is_configured() helper that checks HTTPS_PROXY, HTTP_PROXY,
http_proxy, https_proxy, ALL_PROXY, all_proxy. When DNS fails AND a
proxy is configured, delegate DNS resolution to the proxy rather than
blocking outright.
Blocked hostnames (metadata.google.internal, 169.254.169.254, etc.)
are checked BEFORE DNS resolution, so cloud metadata endpoints remain
blocked regardless of proxy status.
Fixes#32217
Replace 3 duplicated entry_id resolution blocks (try/except +
entry_id_for_api_key + fallback to None) in agent_init.py,
chat_completion_helpers.py, and switch_model with a single
sync_credential_pool_entry_id(agent) function in agent_runtime_helpers.
Follow-up to #70323.
Track the selected credential by stable pool entry ID so token refreshes and shared cursor movement cannot detach failures from the entry that issued them. Stop unmatched single-entry pools from reporting a no-op rotation as successful recovery.
Co-authored-by: Maxim Esipov <maksesipov@gmail.com>
Registers `ar` in the supported-language set and alias table and ships
locales/ar.yaml at full key and placeholder parity with en.yaml, covering
approval prompts and gateway slash-command replies. Identifiers, commands,
paths, config keys, model/provider names, and {placeholder} tokens are kept
verbatim.
Co-authored-by: Da7-Tech <286182457+Da7-Tech@users.noreply.github.com>
Adds the Arabic catalog to the dashboard, registers it in the locale list
and picker, and flips the document direction to RTL when Arabic is active.
Introduces a `defineLocale` merge helper (mirroring the desktop app) so the
Arabic catalog can be a partial override that falls back to English for any
untranslated key instead of hand-porting every future string.
Co-authored-by: morolab <ahmedmoro@gmail.com>
Arabic is the desktop app's first right-to-left locale. The i18n provider
now sets `document.dir`/`lang` from the active locale so Tailwind logical
utilities flip automatically, and `ar` is registered in the catalog,
language options, and alias table. The catalog is a partial `defineLocale`
so keys added to English later fall back cleanly.
Co-authored-by: 3ssiri <assiri@gmail.com>
Co-authored-by: Da7-Tech <286182457+Da7-Tech@users.noreply.github.com>
Follow-ups on the salvaged #54426 routing contract:
- Bare `model` without `provider` on the OpenAI-compatible endpoints
(/v1/chat/completions, /v1/responses) is now opt-in via
gateway.platforms.api_server.direct_model_requests (default off) —
generic OpenAI clients hardcode model names ('gpt-4o', ...) and
existing deployments rely on those falling back to the gateway
default. Explicit `provider` requests and the Hermes-native
session-chat + /v1/runs surfaces are always honored.
Idea credit: PR #22825 by @mssteuer.
- A model_routes alias with no `model` key can no longer leak the
alias string as the executing model name (defensive; parse-time
validation already drops such routes).
- Fix mis-indented _run_agent call args in _handle_session_chat_stream.
- Docs: document the opt-in flag.
Carry model, provider, and model_options through the API server's
execution surfaces (session chat, Chat Completions, Responses, /v1/runs)
without mutating global configuration. Precedence: session /model
override -> model_routes alias -> direct request selection -> global
defaults. Conflicting route/provider mixes fail closed with 400.
model_options stays request-scoped regardless of which selection wins.
Salvaged from PR #54426 by @abundantbeing.
test_docker_network_config.py landed on main after the #58489 revert and
stubbed docker ps with the 2-field ID\tState format. The re-landed
egress-aware reuse probe requests ID\tState\tEgressLabel when egress is
off, so the fake line failed to parse and the reuse path never fired.
Fixture-only change; production behavior is unchanged.
The salvaged #61978 covers tui_gateway/server.py. The crash reported on
Jul 24 came from a sibling site it doesn't touch: the desktop update
panel's _recent_upstream_commits() in hermes_cli/web_server.py runs
git log with text=True and no encoding. Commit 84db32484f put a bug
emoji (UTF-8 f0 9f 90 9b) in a subject on main; byte 0x90 is undefined
in cp1252, so every Windows desktop install behind that commit crashed
in subprocess._readerthread during the update check (#52649).
Guard every text=True capture site in the desktop-backend process with
encoding='utf-8', errors='replace':
- hermes_cli/web_server.py: git log update panel, memory-provider setup
runner, WhatsApp bridge npm install, docker probe
- hermes_cli/banner.py: all 5 git sites (update check runs at startup)
- tui_gateway/host_supervisor.py: build-sha probe, ps probe, compute-host
Popen drain threads
- tui_gateway/compute_host.py: build-sha probe, ps rss probe
The gateway startup maintenance block gained a maybe_auto_archive call in
the same provably-off-loop __init__ site as maybe_auto_prune_and_vacuum;
bump the reviewed sync-escape count from 3 to 4.
Sessions settings gain an "Auto-archive stale chats" toggle with a
configurable idle threshold, persisted to sessions.* in config.yaml so
the backend sweep owns the policy. Sidebar pins (localStorage) are
mirrored to the backend pinned flag at boot and on every change —
pre-existing pins migrate transparently — so the sweep can never hide a
pinned chat.
New sessions.auto_archive / auto_archive_days config: soft-hide (never
delete) sessions with no activity for N days, aging on last activity
rather than creation so an old-but-active chat is spared. Sweeps are
throttled through state_meta and fire from CLI startup, gateway startup
+ hourly housekeeping, and the serve/dashboard backend (opportunistic
on session list + an hourly lifespan ticker), so every surface honours
one setting.
A new pinned column (declaratively migrated) exempts sessions from the
sweep; PATCH /api/sessions/{id} accepts pinned and flips the whole
compression lineage as a unit, mirroring set_session_archived.
Group the flat recents list and entered-project lanes by recency: an
unlabelled head of the newest run of sessions (cut at a real break in
activity, sized toward the most recent handful), then one divider per
coarse calendar range — Earlier today / Yesterday / Earlier this week /
Last week / Earlier this month / month / month + year. Empty ranges are
skipped, the first rendered group is never labelled, branch clusters
never split, and hand-ordered lists / pinned / project previews stay
divider-free.
Nine bundled and optional skills had stale flags, install URLs, packages, and paths. Verified each against upstream and corrected:
- vllm: removed bogus --enable-metrics/--metrics-port (metrics at /metrics on API port); --speculative-model -> --speculative-config; canonical HF model IDs
- lm-evaluation-harness: --tasks list -> lm-eval ls tasks; --allow_code_execution -> --confirm_run_unsafe_code
- weights-and-biases: wandb.keras import removed -> wandb.integration.keras (WandbMetricsLogger); log_uniform -> log_uniform_values for raw values
- huggingface-hub: upload-large-folder now deprecated; hf papers list -> ls
- openhue: Linux install 404 -> openhue_Linux_x86_64.tar.gz tarball (release repo openhue/openhue-cli, v0.24)
- apple-notes: memo notes -a is a bare flag, no positional title
- excalidraw: upload.py path skills/diagramming/... -> skills/creative/...
- searxng-search: removed Method 3 (searxng-data pip package is a PyPI 404)
- sketch: noted get-shit-done upstream is archived/unmaintained
The design-md skill documented the Apr 2026 (0.1.x) CLI behavior, which
has since drifted:
- Lint rules: the skill listed 7 rules that no longer exist by those
names (duplicate-section, invalid-color, wcag-contrast,
unknown-component-property); the 0.3.0 linter runs 9 rules
(contrast-ratio, orphaned-tokens, missing-primary, missing-typography,
section-order, unknown-key, token-summary, missing-sections,
broken-ref). Verified against live lint output.
- Colors: any CSS color is now valid (oklch/rgb/named), not hex-only.
- Export: json-tailwind (v3) + css-tailwind (Tailwind v4 @theme CSS)
formats; 'tailwind' is a back-compat alias. New exit-code semantics
(export exits 0 regardless of source lint findings).
- Section order / duplicate headings are lint warnings, not file
rejection (verified: duplicate + out-of-order sections exit 0).
- Windows: documented the designmd dot-free bin alias (the design.md
bin name collides with the .md file association); skill declares
platforms: [windows].
- New pitfall: typography sub-property typos (fontwight) are silently
dropped with no finding as of 0.3.0.
All claims verified by running @google/design.md 0.3.0 live (lint,
export, duplicate-section, oklch token, starter template lints clean).
Docs page regenerated via generate-skill-docs.py.
Four coding-agent CLI skills drifted from their live CLIs. Verified against live --help/npm and corrected:
- codex: --full-auto deprecated -> --sandbox workspace-write; --yolo -> --dangerously-bypass-approvals-and-sandbox (yolo kept as noted alias)
- claude-code: --effort levels low/medium/high/xhigh/max (dropped removed 'auto', added 'xhigh'); fixed stray table cell
- grok: --session-id is UUID-only for new sessions (cannot resume by name); rewrote the Session Continuation example; noted --max-turns now exists
- blackbox: wrong npm package (@blackboxai/cli is unrelated) -> @blackbox_ai/blackbox-cli; removed dead source-repo link and phantom session/info subcommands
session.branch wrote the child ROW into the parent's profile db but
built the live agent with the launch defaults: _make_agent fell back to
_get_db() and no HERMES_HOME override was active. The branched agent's
own message flushes — and any later compression rotation it performed —
therefore landed back on the launch profile, splitting the lineage one
turn after the branch. Mirror session.create/resume: open the parent
profile's SessionDB for the agent and hold the home override across the
build, so config/skills/memory resolve to the profile too.
Spotted in #70605's sibling implementation of the same fix.
Co-authored-by: HexLab98 <liruixinch@outlook.com>
The blanket MAX_DESCRIPTION_LENGTH=1024->60 change is narrowed:
create-time validation now rejects new skills whose description
exceeds SKILL_PROMPT_DESC_LIMIT (60) with actionable guidance, while
edit/patch paths stay permissive (warning via system_prompt_preview)
so existing over-limit skills remain maintainable. Runtime display
truncation in skills_tool is left at 1024 (display behavior is a
separate concern from authoring validation).
Boundary tests: 60 accepted, 61 rejected at create; edit/patch on
over-budget skills still succeed.
MAX_DESCRIPTION_LENGTH was set to 1024, but the documented skill-
authoring standard specifies <=60 characters. The model generates
descriptions up to 202 chars because the validation allows 1024.
Lower MAX_DESCRIPTION_LENGTH from 1024 to 60 to match the documented
standard. The system-prompt skill index already truncates to 60 chars,
so over-length descriptions lose their routing signal past char 60.
Fixes#52367
Widening pass on top of the #38820 salvage: a full-graph audit of every
SKILL.md (bundled + optional) found 13 more references to skills that
no longer exist. Classes:
- deleted in the 38d3c49aaf bundled-skill cleanup: generative-widgets,
spotify, cloudflared-quick-tunnel, webhook-subscriptions,
debugging-hermes-tui-commands -> dropped
- native-mcp absorbed into the hermes-agent hub skill -> re-pointed
- toolset names that were never skills: browser, image_gen -> dropped
Audit now reports zero broken related_skills references.
Salvaged from PR #38820 by @bedirhancode, re-applied at current skill
locations (obliteratus and s6 moved to optional-skills/ since the PR):
- research-paper-writing: drop ml-paper-writing (never existed)
- touchdesigner-mcp: drop native-mcp (consolidated) + hermes-video (never existed)
- obliteratus: vllm -> serving-llms-vllm, gguf -> llama-cpp
- s6-container-supervision: drop hermes-agent-dev (not a repo skill)
4 of the original 8 hunks were dropped: heartmula already fixed in
#70453; native-mcp SKILL.md deleted from main; architecture-diagram and
comfyui hunks removed refs to concept-diagrams and
stable-diffusion-image-generation, which are valid optional skills.
Auto-gen page slugs, catalog rows/paths, sidebar entries, and zh-Hans
mirrors follow the directory renames. Also updates the install path
official/creative/audiocraft -> official/creative/audiocraft-audio-generation
in the songwriting-and-ai-music pointer section.
Salvaged from PR #42788 by @Love-JourneY, re-applied at current locations
(audiocraft and segment-anything have since moved to optional-skills/):
- skills/mlops/inference/vllm -> serving-llms-vllm
- skills/mlops/evaluation/lm-evaluation-harness -> evaluating-llms-harness
- optional-skills/mlops/models/segment-anything -> segment-anything-model
- optional-skills/creative/audiocraft -> audiocraft-audio-generation
Directory name != frontmatter name breaks skill_view() lookup by dir
name and causes hermes update sync re-seeding duplicates (#42786).
The authoring guide calls this out as Pitfall #8.
Fixes#42786