Commit Graph

6232 Commits

Author SHA1 Message Date
kshitijk4poor
71fe5fccad refactor(model_switch): remove parse_model_flags, which only tests called
Every /model surface parses through parse_model_switch_args. The legacy
5-tuple wrapper had no production caller; its remaining references were two
tests of the tuple shape, an unused import, and two tui_gateway patch()
calls on a function tui_gateway no longer calls.

The en-dash --session normalization those tests covered moves into the
parse_model_switch_args keeper. test_model_switch_once_flags and
test_model_switch_context_offload are removed: the first re-tests the
private parser the keeper already drives, the second the one-line to_thread
wrapper that tests/gateway/test_model_command_context_offload.py proves off
the event loop through the real /model handler.
2026-09-29 15:58:55 +05:30
kshitijk4poor
fbb0eb35e1 fix(cli): missing sticky profile error names the recovery command
When active_profile names a deleted profile, the error still said
"Create it with: hermes profile create <name>", a command that exits with
the same error. Name the command that works instead:
`hermes profile use default`. An explicit -p keeps the create hint.
2026-09-29 15:45:52 +05:30
kshitijk4poor
167d3bb6f3 fix(cli): stale-profile recovery refuses uninstall --full and abbreviated --data
The recovery allowlist checked for the literal string "--data", but the
uninstall subparser accepts argparse abbreviations: `hermes uninstall --dat`
passed the guard and removed the default root's data. `--full` was allowed
outright, and in the default root it rmtree's every other profile under
profiles/ as well, a wider wipe than the --data mode the guard refuses.

Parse the uninstall argv with the real subparser (output silenced; the
handler reports errors itself) and allow it only when neither data nor full
resolves true. `uninstall --help` exits 0 during that probe and is let
through so the handler prints help once instead of help plus an error.
The predicate lives in a topical sibling (main_profile_recovery.py) rather
than growing the main.py facade.
2026-09-29 15:45:52 +05:30
Gille
a4748e9224 fix(cli): allow recovery from missing active profile 2026-09-29 15:45:52 +05:30
unsupportedpastels
536802c00e fix(debug): mask every secret fallback field per agent.redact policy, not only api_key
Reuses the repr-field secret-name policy (is_secret_field_name) so token, password, secret and credential-suffixed fields are masked by field in hermes dump, debug share and /debug. The regression test asserts on what actually reaches the paste service first.
2026-09-29 05:59:25 -04:00
unsupportedpastels
f907520593 fix(debug): mask fallback api_key by field, whatever its YAML shape
Text redaction of str(fallback_providers) missed api_key values that the
runtime still uses (resolve_entry_api_key str()s them): an unquoted numeric
key, a mapping, or a key containing "***" (treated as already masked).
Mask the field before serialising; keep the independent upload-boundary
pass and cover it with #126168's boundary test.

Co-authored-by: John Paul Soliva <soliva.johnpaul@icloud.com>
Co-authored-by: JoaoMarcos44 <joaomarcosdias444@gmail.com>
2026-09-29 05:59:25 -04:00
John Paul Soliva
cf94e3eea3 fix(redact): GCS and Azure SAS signatures count as URL credentials
The strict URL-credential pass that hermes dump and debug uploads use
redacted X-Amz-Signature but not X-Goog-Signature (GCS V4 signed URLs)
or sig (Azure SAS), so a fallback base_url signed that way still reached
the dump and the upload. The documented promise is signed-URL signatures
in general.
2026-09-29 05:59:25 -04:00
John Paul Soliva
139876e727 test(debug): URL credentials in a fallback base_url never reach hermes dump or an upload
Covers userinfo, ?key=, X-Amz-Signature= and #access_token= through the real config.yaml, hermes dump stdout, the share bundle and the /debug report. Docs (EN + zh-Hans) say the dump redacts credentials in fallback URLs.
2026-09-29 05:59:25 -04:00
John Paul Soliva
e70fbbe3ec fix(debug): redact the system dump in debug share and gateway /debug
hermes dump prints config_overrides.fallback_providers as the raw list,
api_key values included, and the upload-time redaction of hermes debug
share and the gateway /debug report covered only the log snapshots, not
the dump text quoted at the top of every paste.

Force-redact the captured dump like the logs (--no-redact still opts
out), and have hermes dump itself redact fallback_providers so a pasted
dump never carries a key.
2026-09-29 05:59:25 -04:00
kshitijk4poor
4e5cea136a test(update): a same-named script outside the checkout still repairs it
Pins the path half of the tail identity: only this checkout's own
hermes_cli/ scripts are exempt from the launch-time repair.
2026-09-29 15:25:13 +05:30
kshitijk4poor
6c7ebd1c01 fix(update): hermes update's own tail no longer starts a second tail
update_completion.py arms the pending marker, then imports hermes_cli.main,
which runs prepare_launch. While the updater's lock claim is live the
ancestor check returns early, but an unwritable claim or one past the
20-minute ceiling sends it into _finish_source_update: a full
source_completion tail runs inside the update and clears the marker before
the update's own build has finished.

Exempt both tail scripts through one identity check. The launch check
already resolves its root, so the comparison is exact on the process's
own argv[0], which the processes a tail spawns do not inherit.
2026-09-29 15:25:13 +05:30
Gille
de48024d75 fix(update): prevent recursive source completion
(cherry picked from commit 481e68381ebb9bbc36dd7b99876b6fd8b833584a)
2026-09-29 15:25:13 +05:30
kshitijk4poor
596678d665 refactor(update): tighten the partial-clone heal after review
- _deepen_shallow_repo marks packs in its finally without a git config probe
  that could itself raise and turn a finished unshallow into False; markers
  are inert in a clone without a promisor remote.
- Installers warn instead of aborting the stage when a marker cannot be
  written, matching the Python helper.
- Docs: the manual commands follow HERMES_HOME, and the missing-object
  backfill feeds IDs through `fetch --stdin` so a large list cannot exceed
  the Windows command-line limit.
- Test and docstring comments describe the contract, not the incident.
2026-09-29 15:24:15 +05:30
kshitijk4poor
282ad01cb5 fix(update): mark unmarked packs instead of the inert promisor retry (#124272)
git 2.53+ `index-pack --promisor` hands every object outside the promisor
packs to pack-objects, which BUG()s (should_include_obj) on the missing
objects those objects lead to. A partial clone with unmarked packs therefore
keeps crashing its fetches.

The retry added for this ran `git -c remote.origin.promisor= fetch`. That
override does nothing: git registers promisor remotes additively, so the
repo's own `true` still wins (GIT_TRACE on 2.50 and 2.55 shows
`index-pack --promisor` and `filter tree:0` under the override). The retry
only passed when the crashed fetch had already stored the pack; a retry that
has to transfer again crashed again.

- On the crash, give every pack without a `.promisor` marker one, then retry
  the same fetch once. The clone stays partial, no refetch, no config change.
  If git still crashes, the updater points at the docs section.
- The two fetches that turn a full clone partial now mark the clone's old
  packs afterwards: the depth-1 `tree:0` unshallow in
  `fetch_full_commit_graph`, and `worktree_ops._deepen_shallow_repo`
  (`--filter=blob:none`). git writes the partial-clone config before it
  fetches, so this also runs when the fetch fails. Without it, a converted
  depth-1 install crashed every later fetch on git 2.55 (3 of 3 rounds in a
  local repro).
2026-09-29 15:24:15 +05:30
Q
70b9cdafd4 fix(update): recognize the git 2.55 partial-clone pack-objects crash
is_partial_clone_pack_objects_crash() required "index-pack failed" next to the
pack-objects assertion, but git 2.55 builds dropped that string: Homebrew git
2.55.0 (macOS) only reports "fatal: fetch-pack: invalid index-pack output", so
the recognizer always returned False there, fetch_with_partial_clone_recovery()
never retried with the promisor machinery disabled, and `hermes update` exited 1
on the crash (#125138 reports the same bare-assertion failure on 2.55.0.windows.5).

Fingerprint on the assertion text — the only version-stable part of the failure
— and accept the 2.55 wrapper as a terminator. The POSIX/Windows shapes and the
"fingerprint without a terminator" rejection behave as before.

(cherry picked from commit 74103b15ceb3eacb2346fca669c3a3dc81457201)
2026-09-29 15:24:15 +05:30
kshitijk4poor
af26acab73 fix(deps): gate the runtime takeover on genuinely historical updater frames
The salvaged matcher accepted any cmd_update/_cmd_update_impl frame in
hermes_cli.update_cmd/main, so the CURRENT updater's entrypoints (which
carry pre_update_version on opts, not as a local) would also hand off and
exit the process mid-`hermes update`. Extract
_old_updater.in_historical_update() sharing _historical_context's exact
frame matcher (module, name, and pre_update_version in co_varnames) so the
shim and the context detector cannot drift apart.

- tools/lazy_deps: use the shared predicate; accept the constraints/
  dry_run kwargs historical updaters pass so they reach the handoff
  instead of dying on TypeError; drop the vague message advice.
- tests: collapse 29 parametrize cases to the real invariants and add the
  missing one — the frame walk is now exercised through a nested helper
  (a getframe(1)-only implementation fails), and a current-updater-shaped
  frame (no pre_update_version local) is asserted NOT to hand off.
2026-09-29 15:08:07 +05:30
funky-xamarin
5e6165562b fix(deps): refuse runtime lazy installer update takeovers 2026-09-29 15:08:07 +05:30
kshitijk4poor
7728574ad4 test(cli): fold foreign-venv wrapper case into the migration test
The standalone ownership test duplicated the parametrized boot migration
(same wrapper, weaker check), was the only test in tests/hermes_cli calling
the private _publish_conveniences, and asserted a mtime-derived "changed"
flag immediately after writing the file, which is flaky on filesystems with
coarse timestamps.

Keep its one unique invariant, another checkout's venv/bin/hermes is never
adopted, by pointing the migration test's existing foreign hermes-agent
wrapper at another checkout's venv. It still also mentions this install in a
comment, so both rejection cases stay covered through public expose_cli().
2026-09-29 14:36:55 +05:30
Gille
91986da496 fix(cli): adopt legacy venv hermes wrapper
(cherry picked from commit ccdbc32d93358c1d0c8219ab3e9441a8a49fdbee)
2026-09-29 14:36:55 +05:30
Siddharth Balyan
3a439b69ad Free tier: separate 'account exists' from 'runs this chat', and stop the mint from writing active_provider (#127486) 2026-09-29 07:44:13 +00:00
Austin Pickett
0f50687327 fix(update): report a symlinked other-install profile gateway as external, not unaccounted
A profiles/<name> symlinked to a separate Hermes install is inventoried by the
update plan, left alone by the restart phase, and already classified
'external' by the post-restart fleet matrix. Reconciliation had no such
outcome, so the gateway ended 'unaccounted' and every hermes update exited 1.

Feed the fleet's verified external gateway pids into match_runtime_outcomes:
an untouched gateway in that set is 'external', reported informationally and
excluded from the exit-1 tripwire. No fleet evidence keeps it unaccounted.

Fixes #120240
2026-09-28 21:04:13 -04:00
Austin Pickett
d619b67ec2 fix(local-runtime): spill only the recurrent FFN blocks a hybrid needs
A spilled hybrid got `-ot blk\.\d+\.ffn_.*\.weight=CPU`, which moves every
block's FFN — full-attention blocks included — regardless of how many bytes
actually had to leave the GPU. The GGUF reader now records FFN weight bytes
per block, and spill_overrides picks the fewest recurrent (n_head_kv==0)
blocks whose FFNs cover the decision's spill_bytes, emitting them as an
explicit `blk\.(i|j|...)\.ffn_.*\.weight=CPU` selector. Unknown sizes fall
back to every recurrent block, never a full-attention one.

Based on #120778 by @kvnloo (recurrent-index selector).

Covers the placement half of #113329; the overhead/logits calibration
(RUNTIME_OVERHEAD_BYTES, ub_logits_bytes) is untouched pending measured data.
Refs #113329
2026-09-28 21:02:39 -04:00
Austin Pickett
f6df13bac6 fix(update): keep never-pushed and unmerged branches pinned in source check
An empty ls-remote advertisement cannot tell "merged and deleted upstream"
from "never pushed". The source check now re-pins a missing branch to main
only when the local branch was once published (remote-tracking ref or a
configured upstream) and git cherry finds no commits missing from main.
Otherwise it keeps the branch and reports branch-local-only.

Diagnosis from #105053 by @0xalydev and #105055 by @kokhlo, which targeted
the Electron healer since moved into hermes_cli/source_check.py.

Fixes #105042
2026-09-28 21:02:35 -04:00
Austin Pickett
90b0b21537 fix(update): record Desktop's correlation_id in the update receipt
Desktop's managed SSH update exports HERMES_UPDATE_CORRELATION_ID and
accepts only a receipt whose correlation_id equals it. The backend never
read the variable, so every successful remote update was reported as
failed and restored. Persist it as correlation_id when the receipt is
created, and keep it across the interpreter handoff.

Idea from @LordMelkor on the issue.

Fixes #101516
2026-09-28 21:02:29 -04:00
Austin Pickett
81f481b2db fix(agent): carry a custom provider's extra_body into TUI/Desktop agents and aux calls
TUI/Desktop `_make_agent` (plus `hermes -z` and ACP) built AIAgent without the
resolver's `request_overrides`, so a custom entry's `extra_body` only reached
the agent through the init-time re-derivation, which matches `custom:*` alone.
Auxiliary requests never read the entry's `extra_body` at all. A proxy that
requires a body field (`user`) therefore 400'd in the app and on smart
approval while `hermes chat` worked.

- Pass `runtime["request_overrides"]` at every remaining AIAgent build site
  (ACP only when no explicit base_url points at another endpoint).
- `_build_call_kwargs` layers the destination entry's `extra_body` under the
  task/caller body, using the agent's own matcher, so primary, retry and
  fallback requests each get their own endpoint's body and nothing leaks.

Covers holes 1 and 3 of #103738; hole 2 (matching bare/unprefixed names) is
#124290 and is untouched here.
2026-09-28 19:49:00 -04:00
Eva
c2e2150d78 fix(sessions): close the identity gaps the review found and prove the recovery paths
- Strip persistence-only fields from a select_context() selection: the request copy was stripped before
  the hook, but an engine handing back the conversation_messages clones put timestamp, _row_id and the
  new ids on the wire (pre-existing for the old fields).
- Every row carries a message_uid whoever wrote it: an AFTER INSERT trigger mints one for a row inserted
  without it (an older build writing into a v31 store); the one-time backfill is gated by its own
  state_meta marker so a store whose schema version cannot advance (no FTS5) never rescans.
- One merge-witness helper (record_absorbed_message) for every fold of a durable message: alternation
  repair's user and assistant merges, the compressor's in-flight restatement onto the carrier, the real
  user anchor folded into a scaffolding turn (the anchor's uid leads), micro-compaction's adjacent-user
  merge.
- tool_call_uids follows tool_calls: it is a payload column, so an assistant merge's unioned map is
  rewritten with the row instead of being overwritten by the stale stored map on adoption.
- A result pairs with the nearest call naming its provider id: each assistant row's ids shadow earlier
  occurrences in the batch, restore and live-list indexes, a call row without a map leaves its result
  unpaired, and nothing pairs across a user turn.
- A composite rewind returns and installs the replacement row's uid on the live scaffold.
- Tests: import_foreign_history, hermes sessions recover and backup import carry the ids; the ACP
  restore test accounts for the uid on restored rows.

(cherry picked from commit b30636a49cb29fdd21ba4100eef9d810897338be)
2026-09-29 04:15:21 +05:30
unsupportedpastels
d565acef9f fix(desktop): route the fail-open cron ticker through the shared profile gate
The fail-open built-in path gated ticks via can_dispatch, so a skipped
tick still recorded a successful heartbeat and cleared the tick error on
the store a live gateway owns. Tick this backend's own store through the
multiplex loop with the same profile_gate instead: a gated-out profile is
neither ticked nor heartbeated.

One _gateway_owns_cron(name, home) predicate now backs every path, so the
fail-open and external-provider paths also stand down for a satellite
served by the live default multiplexer. The external-provider wait probes
once per interval.

Tests: split the external-provider stand-down test and drop the iterator
that hid over-probing; pin the fail-open start kwargs exactly; cover the
satellite gate and the heartbeat through the real built-in loop.

(cherry picked from commit b9250805e7eb951632e9f56e1ce6321e9cbcb395)
2026-09-28 18:44:52 -04:00
unsupportedpastels
284f849d56 fix(desktop): gate the fail-open cron paths per tick instead of standing down for good
The multiplex ticker already yields to a live gateway through its per-tick
profile_gate. The fail-open paths (profile enumeration failure, empty served
set, external provider) still took one ownership probe at backend start and
returned for good, so Desktop cron stayed dead after that gateway stopped.

The built-in single-store ticker now gets the same probe as a per-tick
can_dispatch gate: it stands down while a gateway owns this home, fires once
it stops, and stands down again if it returns. An external provider takes no
per-tick gate, so its start waits, re-probing every interval and honoring
stop_event, until the gateway is gone.

Fixes #126822

Co-authored-by: liuhao1024 <sunsky.lau@gmail.com>
(cherry picked from commit 4ffe4b3fd68b96fd48c4e8847344a69de7c4f0c2)
2026-09-28 18:44:52 -04:00
AhmetArif0
b94716f2db fix(desktop): a gateway live at backend start no longer stops Desktop cron for good
29ca3f2770 bailed out of _start_desktop_cron_ticker whenever a gateway was
live on the home at backend start. The ticker is started once per backend
lifetime, so when that gateway later stopped (Desktop's own gateway stop,
a crash) nothing ticked cron until Desktop restarted. The multiplex
ticker's per-tick profile_gate already stands down while a gateway runs,
so the startup stand-down now applies only to the ungated fail-open paths
it was written for.

(cherry picked from commit ad59becf079f388667620481276f55b8421edcb3)
(cherry picked from commit f2240b871d045f85af100b343bb4a053663fff86)
2026-09-28 18:44:52 -04:00
Teknium
9bcbe7b5df feat(i18n): pluggable, layered language packs across core, Desktop and TUI (#126296)
* feat(i18n): layered catalogs — plugin packs and user overlay over bundled locales

* feat(tui): i18n layer — en catalog, nanostore runtime, RPC pack loader, _keys.tui.json emitter

ui-tui/src/i18n/: en.ts (facade over topical siblings under en/), types.ts
(Translations + dotted TranslationKey derived from en), runtime.ts ($locale/
$catalog atoms, translateFrom active→en→key, pack merge with string→fn
wrapping for {0}/{1} placeholders), loader.ts (display.language →
i18n.catalog {lang, surface:'tui'}, English when the method is missing),
useT()/useLocale() hooks, t() for non-React code. useConfigSync feeds the
loader from the existing config.get full hydration. `npm run i18n:keys`
writes locales/_keys.tui.json (sorted flat key list) and runs before build.

* feat(plugins): provides_locales manifest field, ctx.register_locale/register_locale_dir, manifest-only language packs

* chore(tui): split en catalog siblings by lane (slash sibling)

* feat(plugins): validate language packs — parse, text-only, key-subset WARN against en / _keys exports

* feat(tui_gateway): i18n.languages / i18n.catalog RPC + regenerated contracts

* feat(config): display.language accepts any supported_languages() id, refuses unknown ids with the list

* docs(i18n): language packs user guide, pluggable display.language, plugin developer section, AGENTS notes

* feat(plugins): report language-pack layers in the mid-run activation summary

* feat(tui): wire status bar, composer placeholders, hotkey help and approval/clarify/confirm prompts through i18n

StatusRule maps compared state values (ready/running…/summoning) to catalog
text at render via displayStatus(); hotkeys()/placeholder() resolve lazily so
a pack that arrives after boot applies. Catalog grows to 81 keys.

* feat(desktop): pluggable app locales — registry, host.i18n.registerAppLocale, backend packs, keys emitter

- Locale widens to string (BundledLocale keeps the union); TRANSLATIONS stays
  the bundled record and every consumer resolves through the registry.
- src/i18n/registry.ts: registerAppLocale(id, {endonym, rtl, translations})
  layers partial packs (nested or flat dotted) over bundled/en via
  mergeTranslations; a string over a function-valued en entry becomes a
  positional {0}/{1} formatter; $appLocaleVersion bumps so translators
  re-render; per-source disposers + replaceAppLocaleSource for atomic swaps.
- Backend packs: i18n.languages + i18n.catalog {surface:'desktop'} feed the
  registry as source 'backend' (method-not-found is silent); re-synced on
  socket open, display.language change and profile switch. A saved pack-only
  language is promoted once its pack registers.
- SDK: host.i18n.registerAppLocale / languageOptions; ctx.i18n.registerAppLocale
  tracked for unload. Docs in the desktop plugin SDK guide + skill reference.
- Language switcher lists bundled ∪ registered ∪ backend, endonym-only; RTL
  from the registry (applyDocumentLocale takes rtl).
- npm run i18n:keys emits locales/_keys.desktop.json (wired into build).

* i18n(cli): route /topup + /subscription copy through t() (cli.billing.*, cli.subscription.*)

Module-level copy tables and modal choice tuples in cli_billing_mixin.py froze
English at import, before display.language was known. They are now key tables /
builder functions evaluated at call time; every user-facing line in the /usage
balance block, /subscription and the five /topup screens reads the catalog.
Choice VALUES stay English identifiers. Fragment-assembled status lines
(Plan: … → cancels · $x left · renews …) become full templates.

* i18n(gateway): exec-approval card contract + base/run/run_busy/run_inbound replies through t()

- base_exec_approval: EA_* English constants stay; add ea_header_text()/ea_reason_label_text()/
  ea_smart_deny_line_text()/ea_default_reason_text()/ea_action_labels()/approval_timed_out_notice()
  accessors; deadline + timed-out notice resolve via gateway.exec_approval.*
- BasePlatformAdapter._EA_HEADER/_EA_REASON_LABEL/_EA_SMART_DENY_LINE/_EA_ACTION_LABELS become
  properties (adapters still shadow them with markup class attrs)
- run.py: provider error replies table holds catalog keys; _CONTEXT_OVERFLOW_REPLY -> _context_overflow_reply()
- run_busy/run_inbound: typed approval + slash-confirm matchers accept English ∪ approval.inputs.* (t())
- locales/en.yaml: gateway.exec_approval/busy/errors/... namespaces

* i18n(cli): wire modal, loops, agent-setup mixins through t() (cli.* keys)

* i18n(platforms): route Slack, Matrix and Feishu user-facing text through t()

Exec-approval markup overrides (_EA_HEADER/_EA_REASON_LABEL/_EA_SMART_DENY_LINE/
_EA_ACTION_LABELS) become per-call properties over the shared
gateway.exec_approval.* contract keys, so Slack's 3000-char section budget
measures the resolved template. Slack _APPROVAL_DECISIONS/_CONFIRM_DECISIONS,
Feishu _APPROVAL_LABEL_MAP and Matrix _EA_LEGEND/_EA_TYPED_HINT turn into
key tables resolved at click time; the Matrix typed hints become whole
sentences per offered tier instead of spliced fragments. Slack button labels
are cut to 75 chars and select placeholders to 150 after translation; the
model-facing clarify fallback answer ('choice N') stays English while the
card copy localizes.

locales/en.yaml gains the gateway.exec_approval.* contract keys plus the
platform.shared.* / platform.slack.* / platform.matrix.* / platform.feishu.*
namespaces (and the keys for the other adapters wired in follow-up commits).

* i18n(gateway): run_turn / run_turn_runner / approval-settle copy through t()

- status hints, proxy errors, background task notices, progress heartbeats, session info lines
- tool progress chrome (tool_head/tool_pending/tool_preview/tool_verbose) shared by base.format_tool_event
- run_turn_runner:1406 Chinese clarify placeholder -> gateway.clarify.native_stream_placeholder (zh text kept in zh.yaml)
- _UNEXPECTED_SILENCE_REPLY/_CLARIFY_EXPIRED_NOTICE -> accessor functions

* i18n(platforms): route Google Chat and Teams user-facing text through t()

Google Chat clarify card, typing placeholder, orphan-card labels and the whole
/setup-files reply set (module constants become platform.google_chat.setup_files.*
keys resolved at reply time). The attachment-fallback notice that shipped
hardcoded in Spanish is keyed with an English en value; es.yaml carries the
original Spanish text for those four keys.

Teams approval card header/reason use the gateway.exec_approval.* contract,
_APPROVAL_LABELS becomes a key table resolved at click time, and the meeting
summary writer resolves its section headings/fallbacks per render.

* i18n(platforms): route LINE, WeCom, email, DingTalk, IRC and Home Assistant text through t()

LINE default copy constants become catalog keys resolved in __init__ (the
LINE_*_TEXT / extra.* operator overrides still win); the busy-ack bypass
matcher keys on the leading emoji marker only, so it keeps firing once the
gateway busy heads are localized. WeCom media size/format notices that shipped
hardcoded in Chinese are keyed with English en values and zh.yaml carries the
original Chinese text. DingTalk emotion bubbles resolve per send.

* i18n(cli): route /model switch output and -q status lines through t() (cli.model.*, cli.single_query.*)

Switch-summary labels shared with the gateway reuse gateway.model.* keys
(provider/context/max-output/capabilities/prompt-caching); CLI-only variants
(glyph or no-backtick forms) live under cli.model.*. The hand-padded /model usage
block becomes a (form, description-key) table padded at render time so the
command syntax stays fixed while descriptions translate. -q 'Error:' reuses
gateway.model.error_prefix.

* i18n(cli): route TUI panel/hint/placeholder copy through t() (cli.tui.*)

_APPROVAL_CHOICE_LABELS and _TUI_MODAL_HINTS become key tables resolved at
render time; vault/sudo panel bodies are one catalog value per panel split on
newline; inline plurals use <key>_one/<key>_other. Adds the cli.* namespace
(shared/tui/voice/render/subagents/dock) to locales/en.yaml.

* i18n(cli): voice/wake-word CLI copy through t() (cli.voice.*)

RuntimeError texts raised in _voice_start_recording are human copy (callers
print {e}) and are keyed; the Termux requirement-check match stays English.
Wake state ids stay internal, only their labels localize.

* i18n(cli): live-work dock, subagent monitor and render copy through t()

cli.subagents.* / cli.dock.* / cli.render.*; count fragments pluralize via
_one/_other keys, verdict table holds keys resolved at paint time so width
clipping measures the translated text.

* i18n(gateway): unauthorized/pairing, voice, topics, shutdown, startup, notifications, kanban pings through t()

* i18n(cli): move tips + composer placeholders into the catalog (tips.tNNN / tips.placeholder.pNN)

get_random_tip()/get_random_composer_placeholder() pick a key from the English catalog
(the parity baseline, probed once per process) and resolve it through t() for the active
language, so language packs translate tips like any other string. Also lands the cli.*
en.yaml namespace consumed by the CLI info/help/error-copy wiring in the next commit.

* i18n(cli): wire chat-turn + session mixins through t(); kanban log trimmer matches t() output

* test(cli): assert TUI/dock/voice copy via t(key); prove labels resolve at render time

Pinned-English assertions in the approval-UI, live-work dock and voice tests now
go through the catalog. New test swaps the catalog after import and checks the
approval panel + hint row follow it (the reason _APPROVAL_CHOICE_LABELS and
_TUI_MODAL_HINTS became key tables).

* i18n(cli): route CLI info/help/error copy through t() (cli.* namespace)

cli_info_mixin: /help consumes CommandDef.describe() (added to commands.py: slash.<name>.description
with fallback to .description), section titles/skill/quick-command headers, /tools, /toolsets,
/usage labels, /context, /whoami, /insights, /gateway status, tool-progress labels, bang-shell
denials, MCP config-watch + /reload-mcp confirm/reload lines, /reload-skills, and the session-store
warning all read the catalog at call time (module-level label tables became functions so the
active language is honoured after startup). cli.py: worktree cleanup, tirith warning, show_config
(labels re-padded at print time), quick/plugin/skill slash-command errors, ambiguous-command hint,
stdin error, gateway start, profile warning. cli_chat_error_copy / cli_unknown_command /
cli_output / cli_init_mixin: chat error panel copy, did-you-mean lines, n-more / yes-no prompt
(localized affirmative initial alongside 'y'), unknown-toolsets warning.

* i18n(w1a): wire agent display/explainers/approval + slash registry/help through t()

- hermes_cli/commands.py: CommandDef.describe() resolves slash.<name>.description
  at call time; category labels via slash.category.*; help/alias/usage suffixes
  via slash.shared.*; gateway_help_lines and commands_platforms/slash_exec use them.
- agent/display.py: display.verb.* resolved at call time via get_tool_verb();
  bridge/spinner/thinking-verb/cute-row/failure/preview/diff text via display.*.
- agent/turn_explainers.py: exit-reason / persistence-cause tables become call-time
  lookups (explainer.exit.*, explainer.persistence.*, explainer.file_mutation.*).
- agent/background_review.py, session_activity.py, context_breakdown.py,
  status_output.py: review summaries, iteration progress, context notices.
- tools/approval.py, approval_context.py: approval.summary.*, approval.noun.*,
  approval.window.* pluralized keys.
- gateway/slash_commands*.py: remaining raw strings (busy, whoami, platform,
  bundles, memory, skills, approvals, set_home, diff, update, debug, profile,
  heartbeat, refine, review, subgoal, loop, retry, compress codex path, save,
  sessions, model guard/errors, agents rows, topup, login). HISTORY_UNREADABLE
  keeps its English constant; callers use history_unreadable() ->
  gateway.shared.history_unreadable.
- locales/en.yaml: new approval/display/explainer/slash blocks + gateway leftovers.

* i18n(telegram): route adapter chat copy through t()

Approval card (header/reason/smart-deny as HTML-escaped properties), inline
button labels, callback toasts (cut at Telegram's 200-char cap), model/choice
pickers, clarify/update/slash-confirm prompts, gmail-triage labels and the
inbound-media failure notice now come from the catalog. _UNAUTHORIZED is a
lazy _unauthorized() so the import no longer binds a language. The command
menu carries a language+payload fingerprint (forum scopes re-register on
change) and BotCommand descriptions are cut at 256.

Adds gateway.exec_approval.* (WAVE2 contract), platform.telegram.*,
platform.discord.* and the slash.*.description keys the Discord table shares
with the CLI registry to locales/en.yaml.

* i18n(gateway/platforms): whatsapp_cloud, yuanbao, weixin, signal, api_server copy through t()

- whatsapp_cloud: clarify list/buttons, approve/deny + slash-confirm labels via platform.whatsapp.* (t()-then-truncate at 20/24/72 caps); _EA_HEADER becomes a property wrapping ea_header_text()
- yuanbao: SLOW_RESPONSE_MESSAGE -> slow_response_message() (platform.yuanbao.slow_response_notice; zh keeps the original text); cron-wrapper markers centralized as module constants for strip_cron_wrapper
- api_server: PROVIDER_AUTH_FAILED_LABEL/PROVIDER_RATE_LIMITED_LABEL stay English for run.py matchers; user_text() renders via t()
- signal/_format_wait, weixin voice caption, openai_routes transformed notice
- run_turn: second _UNEXPECTED_SILENCE_REPLY consumer -> accessor

* i18n(discord): route adapter chat copy through t()

Native slash-command table becomes _NATIVE_SLASH_COMMAND_SPECS holding catalog
keys; _native_slash_commands() resolves descriptions, parameter descriptions
and Choice names for the active language, each cut at Discord's 100-char cap,
and the app-command sync fingerprint now includes get_language() so a
display.language change re-syncs. Exec-approval card (gateway.exec_approval.*
contract), slash-confirm / clarify / update views, model+choice pickers,
thread creation, forum titles, voice acks, the response-truncation notice,
the unauthorized-slash security alert and the media upload-size notices all
read from platform.discord.*. Decorator-declared button labels are relabelled
in __init__ (80-char cap); embed titles cut at 256, select placeholders at
150, option label/description at 100. _UNAUTHORIZED is a lazy _unauthorized().

* i18n(cli): wire status-bar, stream, terminal mixins + terminal_input through t(); rename kwargs that shadow t(key)

* i18n: wire hermes_cli/cli_commands_mixin.py slash-command copy through t()

- 431 new leaves under cli.commands.<cmd>.* in locales/en.yaml; 12 rows reuse
  existing gateway.* keys (rollback, diff, resume, branch, btw, model, reasoning)
  via a _gt() helper so CLI and gateway replies stay identical.
- Module-level English tables (_BUSY_MODE_*, _REASONING_TOGGLES, _HATCH_PROGRESS,
  _DIFF_LABELS, _LOCAL_ENGINE_LINES) become call-time catalog lookups keyed by id.
- Verb tables (Enabling/Disabling, Paused/Resumed/Triggered, planned/done,
  Updating/Generating) are one full template per variant; plurals use
  <key>_one/<key>_other via _tn(); hand-padded column labels (/snapshot list)
  translate the value and re-pad at the call site.
- Multi-line usage blocks are single catalog values split with _lines().
- Model-facing system notes and DB-stored reasons stay English (EXCLUDED).

* tests: assert /handoff, /worktree, /login CLI copy via t(key) instead of pinned English

* test(i18n): pin Telegram/Discord adapter catalog wiring

Lazy unauthorized notice, exec-approval contract keys, HTML escaping before
Telegram <b> wrapping, 200-char toast / 256-char BotCommand caps, Discord
100-char app-command text and 80-char button caps, and language-bearing
command-menu fingerprints on both platforms.

* i18n: reconcile cli.shared on/off vs enabled/disabled after lane merge

* i18n: describe() in TUI-gateway slash listings; localize TUI exit resume hint

* i18n(tr): translate bundled catalog + tui pack

* i18n(ja): translate bundled catalog + tui pack

* i18n(ko): translate bundled catalog + tui pack

* i18n(zh): translate bundled catalog + tui pack

* i18n(fr): translate bundled catalog + tui pack

* i18n(af): translate bundled catalog + tui pack

* i18n(uk): translate bundled catalog + tui pack

* i18n(ar): translate bundled catalog + tui pack

* i18n(pt): translate bundled catalog + tui pack

* i18n(it): translate bundled catalog + tui pack

* i18n(es): translate bundled catalog + tui pack

* i18n(zh-hant): translate bundled catalog + tui pack

* i18n(ru): translate bundled catalog + tui pack

* i18n(hu): translate bundled catalog + tui pack

* i18n(hu): translate pre-existing English-valued leftovers (kanban wake, /context, /status, fast labels)

* i18n(de): translate bundled catalog + tui pack

* i18n(ga): translate bundled catalog + tui pack

* test(i18n): fixture matches _normalize_lang(lang, home) signature

* i18n(tui): scaffold userMessages/slashCmd en siblings

* i18n(tui): wire secure prompts + content tables

* feat(tui): i18n — wire billing, subscription, connection-setup and journey overlays

Adds en siblings billing.ts / subscription.ts / connection.ts (namespaces
billing, subscription, connection, journey) and routes every user-facing
literal in billingOverlay, subscriptionOverlay, connectionSetupOverlay and
journey through useT()/messages(). Module-level label tables became lazy
(scopeStillDeniedResult(), verbOf(T, action)); auto-reload rows dispatch on
stable ids instead of label text. Regenerates locales/_keys.tui.json.

* i18n(tui): wire slash ops/wake replies

* i18n(tui): wire pickers (modelPicker, activeSessionSwitcher, petPicker)

* i18n(tui): wire slash core/debug/setup replies

* i18n(tui): wire hubs (agents overlay/panel/controls, skills, plugins)

* i18n(tui): wire slash session/topup/subscription replies

* i18n(tui): wire chat bits (branding, thinking, messageLine, loaders, todo, queued, banner, entry)

* i18n(tui): register t3 siblings (pickers, hubs, secure, content, chatBits) and regenerate keys

* i18n(tui): wire userMessages copy through the userMessages namespace

* i18n(tui): lazy-copy test for userMessages, regenerate _keys.tui.json

* i18n(tui): wire session/gateway/lib text through the TUI catalog (lane t2)

Adds en siblings session.ts, gatewayMsg.ts, libText.ts (namespaces session,
gatewayMsg, libText) and routes user-facing literals in app/{useMainApp,
useSessionLifecycle,useInputHandlers,turnController,createServerRequestHandler,
setupHandoff,createGatewayEventHandler}.ts, gatewayClient displayed reasons,
lib/*, domain/*, hooks/* through t()/messages(). Status-bar state values that
code compares against, backend-matched strings, log lines, model-bound text,
and machine 'error:' prefixes stay literal. Regenerates locales/_keys.tui.json
(232 keys).

* i18n: serve bundled locales/<lang>.tui.yaml under overlay/packs; TUI pack parity test; regen _keys.tui.json (1250)

* i18n: translate pre-existing English stubs in bundled locales (424 leaves, 14 locales)

* tui: i18n-export-en script (English templates for pack translators)

* docs(i18n): bundled TUI packs are the bottom layer of the tui surface

* i18n(ru): translate TUI pack

* i18n(ar): translate TUI pack

* i18n(es): translate TUI pack

* i18n(pt): translate TUI pack

* i18n(ko): translate TUI pack

* i18n(de): translate TUI pack

* i18n(ja): translate TUI pack

* i18n(fr): translate TUI pack

* i18n(tr): translate TUI pack

* i18n(it): translate TUI pack

* i18n(zh): translate TUI pack

* i18n(zh-hant): translate TUI pack

* i18n(hu): translate TUI pack

* i18n(uk): translate TUI pack

1,169 missing keys translated; 81 pre-existing kept byte-identical. Parity OK missing=0 extra=0 placeholder_mismatch=0 empty=0.

Deliberately identical to en: chatBits.branding.mcpSummary ({0} MCP), chatBits.thinking.agentsHint ((/agents)), session.main.voiceStt (◉ STT), session.main.voiceTtsSuffix ( [tts]), slashCmd.core.help.tuiSection (TUI), slashCmd.core.history.hermesTag (Hermes #{0}), slashCmd.debug.heapdump.heapPath (heapdump: {0}), slashCmd.debug.mem.rss (rss), subscription.stepUp.title (Remote Spending — product feature name, as in core catalog), content.faces.* (glyph-only kaomoji).

* i18n(ga): translate TUI pack

* i18n(af): translate TUI pack

* plugin_guard: locale catalogs in language packs step down the agent-config family

A translated status line such as "Updating AGENTS.md" in locales/<lang>.yaml is UI text the loader
reads as a string leaf; it cannot edit a file. The bundled en.yaml itself tripped agent_config_mod
at critical, making any faithful language pack uninstallable. Injection shapes keep full severity.

* plugin_validate_locales: read key exports with utf-8-sig (Windows footgun lint)

* i18n(relay): route relay adapter prompt copy through t(); drop dead import-bound approval header

Adds platform.relay.* (5 keys) to en and all 16 bundled locales, reusing the sibling platform
translations for the confirm buttons and the Other option.

* ci: fix TUI import order, MDX table pipe, main's overflow-warning wording in all locales; fresh-install fixture carries the i18n kernel

- ui-tui/src/i18n/en.ts: perfectionist/sort-imports (slash before slashCmd)
- docs plugins/index.md: escape the | inside the provides_locales table cell (MDX parsed <id> as JSX)
- display.notice.uncompressed_context_overflow: adopt main's wording (names compression.enabled: false
  and /compact) in en + 16 locales; the guardrail test pins that phrase
- tests/scripts/test_fresh_source_install.py: the installer tail now resolves CLI text through
  agent.i18n, so the fixture tree carries the i18n kernel + en.yaml (not the agent runtime)

* docs(desktop-plugin-sdk): double-backtick the template-literal example (MDX evaluated ${n})

* test(e2e): display.language is validated against the live language set; exclude it from the arbitrary-string set property

* commands: keep the localized COMMANDS/COMMANDS_BY_CATEGORY module __getattr__ after the compat block removal

* build: never write locales/_keys.*.json from the desktop/TUI builds; regenerate the committed desktop key export

The desktop build regenerated locales/_keys.desktop.json in the checkout, so a
hermes update that rebuilt the app left the tree dirty (Desktop update E2E:
'M locales/_keys.desktop.json'). The key exports are committed artifacts pinned
to en.ts by apps/desktop/scripts/i18n-keys.test.mjs and ui-tui i18n:keys:check;
builds read them, never write them. Regenerated after main's new desktop strings.

* test: unbreak two main-red timing tests the PR merge-ref inherits

- test_local_runtime racing fake publishes the modern state record (legacy pid-only
  records are rejected since 65ff3ad353; main has been red on this test since)
- test_run_progress_topics ManyProgressLinesAgent waits for the first bubble instead of
  a fixed 0.35s, which a loaded CI runner does not always meet

* chore(i18n): regenerate desktop key catalog for main's new strings (model pricing, copy changelog)

* test(e2e): torture-chamber fd monitor confirms a deleted sidecar is still held before calling it a leak

SQLite's WAL last-close unlinks -shm before closing its descriptor (unixShmUnmap, then
unixShmPurge), so a healthy close shows a (deleted) -shm for microseconds; the 20ms poll
occasionally caught that window on the short-lived opener and failed the episode.

* chore(i18n): regenerate desktop key catalog for main's telemetry/consent strings

* chore(i18n): regenerate desktop key catalog after main sync

---------

Co-authored-by: Teknium <teknium@nousresearch.com>
2026-09-28 14:16:18 -07:00
kshitijk4poor
2b98a469d3 refactor(tests): resolve a guarded path once; tripwire test runs without symlinks
The installed-app check now hands its resolved path to check() instead of
both resolving it, and uses the captured app paths as already normalized.
The symlink case of the tripwire test is added only where the host can
create symlinks, so Windows without Developer Mode still runs the rest.
2026-09-29 02:40:55 +05:30
kshitijk4poor
4c014e4407 test: refuse changing the developer's real installed Hermes desktop app
packaged_gui_app_paths() returns the machine's real install locations
(/Applications/Hermes.app and ~/Applications/Hermes.app on macOS, the
desktop entry and icons on Linux, Programs\Hermes on Windows), and tests
keep HOME intact. A test that runs the real GUI uninstall or update swap
without stubbing it deletes the developer's own app: the variant of
test_uninstall_gui_keeps_userdata_dir_when_told_to in 954d90f15f
(#122553) removed /Applications/Hermes.app on a maintainer machine
during review, and Hermes vanished from Spotlight.

The home I/O guard now captures those paths at conftest import, before
any test can patch sys.platform or HOME, and fails the test on any
delete, rename, replace, chmod, utime or write-mode open inside them,
by the literal or the resolved path. Reads stay allowed (update paths
probe the installed app). The refusal uses pytest.fail
because the uninstall removal helpers catch Exception: an
AssertionError there is logged as a warning and the test passes.
2026-09-29 02:40:55 +05:30
kshitijk4poor
aea6f7a660 refactor(desktop): write the installed-app record only when it changes
Every hermes desktop launch refreshes the installed copies, so the record
was rewritten each time, and recreated as an empty list right after
hermes uninstall. Skip the write when the set is unchanged, check
ownership once up front, and ignore a record whose apps field is not a
list. The test now proves a reinstalled copy is owned by its own stamp
(record deleted first) and that uninstall leaves no record behind.
2026-09-29 02:37:33 +05:30
kshitijk4poor
b52c73c3e4 fix(desktop): hermes update puts back an installed Hermes.app that went missing
hermes update only refreshed installed copies that still existed: once
/Applications/Hermes.app was gone (trashed, deleted by a tool, lost in a
migration) every later update skipped it silently, and Finder, the Dock
and Spotlight never found Hermes again. The ownership stamp that marks a
copy as this checkout's lives inside the bundle, so it left with it.

Record the installed copies the update keeps current in
desktop-installed-apps.json under the default Hermes root. When no owned
copy is left, a recorded path that is now missing still counts as
installed: the update rebuilds for it, reinstalls it and says so. A copy
the user moved to the other Applications folder keeps its stamp, so it
is found instead of doubled. The record is machine-wide, so Hermes' GUI
uninstall deletes it from any profile and an uninstalled app stays
uninstalled. Only the checkout that owns installed apps writes it; a
copy that failed to reinstall stays recorded so the next update retries.

The skip dates from 032cf8438f (#113933), which scoped the refresh to
existing bundles.
2026-09-29 02:37:33 +05:30
teknium1
304cf75bbd fix(review): a second tenant's host gateway never attaches, refuses, or reports as ours (#121352)
The PR filtered a foreign tenant root only in hermes_cli.gateway.host_multiplexer_serving; the
seam `gateway run` actually goes through had no cross-tenant path, so tenant B's gateway still
never started: host_attach.decide() answered ATTACH (exit 0, nothing running) on tenant A's
record "serving default", and run._claim_host_gateway_role lost the per-OS-user host lock to A
and refused 75 forever (a race B can never win).

* gateway/host_attach.py: one predicate, launched_by_other_tenant(owner_home, our_home), used by
  decide() (foreign owner -> START), by the lock-loss branch of _claim_host_gateway_role (start
  beside it, WARNING), by host_multiplexer_serving (replaces its inline copy) and by
  host_topology._from_host_record (doctor / cron status / dashboard ladder no longer report a
  foreign tenant's process as this tenant's host gateway).
* gateway/host_topology.py + gateway/status.py: HostGatewayTopology carries the launch home, and
  multiplexer_liveness_for_profile reads the multiplexer's gateway_state.json from THAT home, so a
  named-hosted multiplexer no longer projects a stale standalone record from the default root.
* hermes_cli/gateway_multiplex_mode.py: recorded_standalone_warning_lines gates on gateway_state
  + live PID only; a paused/wedged-heartbeat live gateway still warns.
* docs: `hermes -p X gateway restart` on a parked profile with no live gateway behaves as start.

Tests: one regression per finding (decide()+lock loss; topology home+tenant filter), both red on
the PR head. tests/gateway/test_host_gateway_lock_refusal.py publishes its record from the
process home instead of an arbitrary tmp dir (the home was never consulted before; assertions
unchanged). test_gateway_multiplex_mode case 3 flips to "stale heartbeat + live PID still warns".
2026-09-28 13:30:57 -07:00
teknium1
d4c14f9093 chore(tests): drop trailing blank line (salvage trim) 2026-09-28 13:30:57 -07:00
lorenzozanee
61bd7e302b fix(gateway): restart parked profiles and report live gateways 2026-09-28 13:30:57 -07:00
teknium1
f1a38955bd fix(cli): served-profile guard only counts a host gateway that is ours and not our own (#121352, #120871, salvage #121574 #122169)
`host_multiplexer_serving` read the HOST-wide rendezvous record and every
CLI guard (`_named_profile_refused_under_multiplexer`, `_cmd_stop`,
`_cmd_restart`, profile_lifecycle, status/cron/dashboard readers via
`named_profile_served_by_running_multiplexer`) trusted it as "someone
serves you", so two gateways that are NOT a multiplexer serving this
profile were refused with exit 78:

* #121352 (T2 vs a second tenant home): two Hermes tenants on one host,
  separate HERMES_HOME roots, each with its own `default`. Tenant B's
  start guard saw tenant A's multiplexer "serving default" and refused;
  launchd parked the unit. gateway/host_attach.decide() and run.py's
  host-lock claim already start beside a foreign-root owner; the CLI
  probe did not. `host_multiplexer_serving` now returns None when the
  record's home resolves to another tenant root
  (`get_default_hermes_root(home=...)` vs ours).

* #120871 (T1 standalone named fleet, T6 launchd): `hermes -p argus
  gateway restart` refused argus's OWN standalone host record as "a
  multiplexer serves you" and pointed at `-p default`, whose gateway was
  not running. `named_profile_served_by_running_multiplexer` no longer
  counts a host record launched by the profile's own home; `cron status`
  names the profile that launched the scheduler host instead of a
  hardcoded `default`.

Control kept: a satellite whose own home is not the host record's home,
while the same tenant's live default multiplexer lists it, is still
refused without --force and the hint names the owner.

Re-implements the idea of #121574 and #122169 by @Finn763 at the shared
seam so every reader inherits it (those PRs patched only one caller each).
2026-09-28 13:30:57 -07:00
teknium1
35e0cac79b fix(gateway): keep the default's flat platforms in the multiplexer re-key (#123088, #123869, salvage #123106)
Trim of the salvaged #123106 commits (@liuhao1024) to the sweep bar: shorter
rationale comments at the three call sites, one-line fold in the messaging
route, and the helper test now also pins the STANDALONE record shape
(`served_profiles: []`, flat keys only) that #123869 reports — the same
re-key through the multiplexer rung projected `{}` there too.

Why one fix covers both issues (and the launchd/bootstrap variant on #123869):
`profile_platforms_from_multiplexer` kept only `<profile>:` keys, but a
multiplex host's primary map is always `default` (run.py `_primary_profile_name`)
and a standalone gateway writes only flat keys, so the one profile the record
never prefixes projected as empty. Both dashboard callers now fold on the
resolved profile NAME (`profile_name_for_home`) instead of a directory
basename, so `own_home.name == ".hermes"` no longer defeats the fold on the
Messaging card. Secondary projection is unchanged: a served profile still sees
only its `<name>:` keys plus the shared listener mirrors.

Not addressed here (different mechanism, tracked on #124029/#123109): the
inline `-c` bootstrap argv making rung 3 (`get_runtime_status_running_pid`)
return None; with this fix that fall-through no longer changes the projection.
2026-09-28 13:30:57 -07:00
liuhao1024
dae66906da fix(gateway): key the status fold on the profile name, not the root basename
?profile=default resolves the default ROOT directory, whose basename
(".hermes" or any custom HERMES_HOME name) is not the profile id, so the
scoped /api/status re-key folded on a name the multiplexer record never
uses — gateway_platforms stayed {} while bare /api/status was fixed, and
the basename read as a named id, fabricating /p/<basename>/ mirror URLs.
Resolve the fold key through profile_name_for_home like the messaging
call site (#123088).
2026-09-28 13:30:57 -07:00
liuhao1024
80c8043a54 fix(gateway): key the messaging fold on the profile name, not the home basename
_platform_payloads passed own_home.name — the directory basename, '.hermes'
for the shipped default root or any custom HERMES_HOME name — into
profile_platforms_from_multiplexer, whose fold only matches a profile NAME.
Only secondaries under profiles/<name> have basename == profile id, so the
default home's re-key came back empty and /api/messaging/platforms kept
reading 'Restart needed' forever while the multiplexer served its flat-keyed
adapters (#123088 follow-up; /api/status already passed 'default'
explicitly).

Resolve the name with profile_name_for_home, and add a regression test whose
fixture home is literally named '.hermes'.
2026-09-28 13:30:57 -07:00
liuhao1024
f373cff9d5 fix(gateway): keep the default profile's flat platforms in the multiplexer re-key
Under gateway.multiplex_profiles the multiplexer record stores the default
profile's own adapters under flat keys — only secondary profiles get the
"<profile>:" prefix. profile_platforms_from_multiplexer kept only prefixed
keys, so every reader that re-keys through it (/api/status) reported
gateway_platforms: {} and components.platforms.configured: 0 for the default
profile while its adapters were connected and delivering.

Fold the record's flat (un-prefixed) entries into the re-keyed map when the
profile is "default" — the standalone shape the helper already promises.
Mirrors are still never built for the default and secondaries keep seeing
only their prefixed entries plus the shared-listener mirrors.

Fixes #123088
2026-09-28 13:30:57 -07:00
teknium1
2dc8f7715a fix(dump): report a supervised gateway without a scannable PID as running (#125390)
hermes_cli/dump.py::_gateway_status indexed snapshot.gateway_pids[0] whenever snapshot.running
was true. Under s6 the service is up but the process scan is empty (python -c launcher argv,
no gateway.pid), so the IndexError was swallowed into "unknown". Same class as the
_cmd_status bug: a supervisor-reported service must not be equated with a PID hit.

Topology T10 (s6/Docker).
2026-09-28 13:30:57 -07:00
teknium1
1b1b93a65c fix(cli): trim s6 status salvage and fold its tests into test_gateway.py (#125390, salvage #125397)
Follow-up to the cherry-picked #125397 by @dskwe: shorten the branch comment and move
the two regression tests next to the existing s6 runtime-snapshot test in
tests/hermes_cli/test_gateway.py instead of a new per-issue test module.

Topology T10 (s6/Docker); the #123881 inline `python -c` argv rejection is untouched —
_cmd_status now trusts snapshot.service_running when the process scan is empty.
2026-09-28 13:30:57 -07:00
Yuan Li
08c0c8c423 test(cli): cover s6-supervised status without a scannable gateway PID
Two regression tests for the #125390 branch: a service_running snapshot
with empty gateway_pids reports the s6 supervision line, and a plain
manual host with no PIDs keeps the stopped message and hints.
2026-09-28 13:30:57 -07:00
teknium1
a17f919afb test(cli): make the #120991 standalone-warning test non-vacuous (salvage #120997)
On a host with no unserved profiles standalone_warning_lines() is empty for
any record, so the stopped/dead-pid/stale cases passed on origin/main too.
Pin unserved_profiles for the whole test so the liveness guard is what is
under test (red on origin/main, green here). Drop the trailing blank line
the PR added to gateway_multiplex_mode.py.
2026-09-28 13:30:57 -07:00
webtecnica
4ee44378f0 fix(cli): verify gateway liveness before emitting recorded standalone warning
Fixes #120991
2026-09-28 13:30:57 -07:00
teknium1
226eeeb4c2 fix(desktop): onboarding metrics follow the intro removal
Main removed the intro cinematic (#126139): the gate no longer has a
`cinematic` phase and store/intro-reveal is gone. The funnel stops recording
an `intro` step (dropped from the contract and v3 schema, never shipped) and
the consent strip no longer waits on the intro film.
2026-09-28 12:43:03 -07:00
teknium1
9b7db3105b fix(telemetry): fewer rows per busy day and a smaller local store
A volume simulation through the real store showed two costs worth cutting.

Rows: hermes.task_run.finished carried duration, retry, model-call and
tool-call buckets on top of outcome/end reason/surface, so almost every task
became its own row; hermes.tool_call.count did the same with latency and
retry buckets. The terminal rows now keep only the fields they are filtered
by, and the same end events feed two small split counters:
hermes.task_run.duration (surface, outcome, duration, retries) and
hermes.tool_call.latency (tool category, latency, retries). Per-task call
counts already ride on hermes.task_cost.count. v3 still accepts the v2 field
sets of both counters so rows recorded before an upgrade drain.

Local store: every package lived three times for 30 days (counter rows, the
payload column, a pretty-printed outbox file). Outbox files are now compact
JSON, and once the ingest has accepted or refused a package the database
drops its copy of the body; the file stays as the local history the docs
promise, and pending packages keep their body for resends.

Measured (simulated day, skewed draws, before -> after):
  heavy    2,788 -> 2,347 rows, 21.0 -> 17.7 KB on the wire, 102 -> 44 MB local over 30 days
  extreme  6,561 -> 5,047 rows, 46.0 -> 33.6 KB, 272 -> 102 MB
  typical    663 ->   650 rows,  7.1 ->  6.8 KB,  24 ->  13 MB
2026-09-28 12:43:03 -07:00
teknium1
1763066334 fix(telemetry): Azure deployment names leave only when they are a public model id
Azure calls a model by a deployment name its owner chose, which can name
a team, customer or environment. On azure* providers the model id now
passes only when it is in Hermes' static catalogs or the local models.dev
cache (never a network call); anything else reads custom.
2026-09-28 12:43:03 -07:00