Commit Graph

46117 Commits

Author SHA1 Message Date
John Paul Soliva
0b9287c8cf fix(desktop): a finished agent terminal stops pinning its backlog for the life of the renderer
Every `terminal(background=true)` mints a new process id, and the agent-terminal
stream module never forgot one. `writers` was unregistered when a tab unmounted,
but `backlog`, `lastSnapshots`, `commandHeaders` and `seededCommands` are keyed
by process id and had no eviction at all, so every command an agent ever ran kept
its output until the window closed.

Replaying each process back through `registerAgentTerminalWriter` measures the
retention directly, without depending on GC: 256K chars held per finished process
(~512 KB, JS strings being UTF-16), growing linearly — 100 processes 25.6M chars,
200 51.2M, 400 102.4M (~195 MB). With this change it is flat at 1.79M chars
(~3.4 MB) for every one of those counts.

Bounded the way the other renderer caches already are (`lib/lru-cache`,
`chat/shiki-highlight-cache`): an entry ceiling and a total-character ceiling,
because one busy process can hold as much as twenty quiet ones. `backlog`
insertion order is the LRU clock, so a chunk write re-inserts to move its process
to the tail — a plain re-set keeps the original slot and would let the
oldest-first sweep evict a process that is still streaming.

A process whose terminal is MOUNTED is never evicted: that is on screen, not a
cache. Everything dropped is regenerable — the next `syncAgentTerminalSnapshot`
re-seeds the tab from the registry's rolling tail. The four maps are evicted
together because `lastSnapshots` is the delta fence for `backlog`; dropping one
without the other makes the next snapshot diff against a tail that is gone and
paint only the delta.

Each guarantee is pinned by a regression that fails when that guarantee alone is
reverted: no eviction, evicting a mounted process, losing the recency refresh,
and keeping a stale delta fence.
2026-09-29 11:41:14 -05:00
BrunoBza
5f23cac568 fix(desktop): warm local STT on mic-open so cold model load stops timing out transcription (#105955) 2026-09-29 11:40:36 -05:00
Siddharth Balyan
a2840f954b Inline agent tools get the shared argument repair (clarify, todo, memory, session_search) (#127781)
* fix(tools): inline agent tools get the shared argument coercion

Inline agent tools (todo_list, clarify, memory, session_search, and the
rest of INLINE_TOOL_EXECUTORS) never went through handle_function_call.
They did not get coerce_tool_args, so a JSON-encoded string for an array
parameter failed unless the tool parsed it by hand.

Every entry of INLINE_TOOL_EXECUTORS now runs coerce_tool_args first.
The sequential path and the concurrent invoke_tool path both read that
table, so one wrapper covers both.

todo_tool no longer parses a JSON string by hand. The todo_list schema
declares todos as an array, so coercion covers it. The existing test now
calls the dispatch table, not todo_tool directly.

* chore: retrigger CI (zero-job dispatch failure, auto-heal)
2026-09-29 22:07:30 +05:30
Mark S.
ca705dbf7e fix(ci): card-tool contract test reads the as-const list and runs on desktop edits (#128010)
#127828 changed CARD_TOOL_NAMES in apps/desktop/src/lib/tool-render-class.ts
from `new Set([...])` to `[...] as const`. The pytest that pins that list
against the gateway's _TOOL_LIFECYCLE_UI_TOOLS located it with a regex that
only matched `new Set([`, so it has failed on main since with
"CARD_TOOL_NAMES not found in tool-render-class.ts". The gateway set still
covers every desktop card name; only the test's parser was stale.

The PR merged green because it touched only apps/, which skips the Python
lane, so the one test that reads that file never ran on it.

- The parser accepts both declaration forms and fails on an empty list
  instead of passing a vacuous subset check.
- tool-render-class.ts joins _PY_RELEVANT_CONTRACT_FILES so an edit to it
  runs the Python lane, with a classifier case for the entry.
2026-09-29 12:27:26 -04:00
Jeffrey Quesnelle
5000e29936 Merge pull request #127012 from RyanUnderhill/fix/n1x-pci-device-identification
fix(local-models): identify N1X by PCI vendor/device ID
2026-09-29 11:55:48 -04:00
ethernet
5bb2be179d Merge pull request #127959 from NousResearch/fix/desktop-native-quad-epoch
fix(desktop): use admitted release epoch for Windows native version
2026-09-29 11:42:16 -04:00
kshitijk4poor
16c59d0e78 perf(cli): scan platform manifests only for names the cheap checks cannot place
saved_toolset_resolver built the hermes-<platform> bundle set eagerly, so
every migrate_config run (hermes update, config migrate, doctor) and every
config check paid a full bundled+user manifest scan even with no saved
platform_toolsets. The scan is now cached and lazy, like the plugin lookup.
The stale-toolset test stubs the plugin lookups so host-installed plugins
cannot change its verdict.
2026-09-29 20:34:45 +05:30
kshitijk4poor
361dc5cf69 fix(cli): judge saved selections the way the runtime does in config check
Follow-up to the config check diagnostics from #127392.

- Saved platform_toolsets are validated by one resolver,
  toolset_validation.saved_toolset_resolver, shared with
  `hermes config migrate`. The two commands previously disagreed: migrate
  flagged MCP server names and plugin-platform bundles (even suggesting
  'hermes-teams' for 'hermes-teams'), while config check accepted them.
- A configured MCP server is known even when disabled (enabled: false);
  it is inactive, not a typo.
- known_plugin_toolsets is no longer treated as proof that a toolset
  exists. It records what a platform was offered at save time, so it hid
  toolsets of uninstalled plugins, the stale case this check is for.
- Plugin-provided names are consulted only for names nothing else
  explains, so a clean config no longer runs plugin discovery.
- A malformed mcp_servers (a list) no longer crashes the command.
- The top-level `toolsets` check is dropped: the CLI resolves its
  toolsets from --toolsets or platform_toolsets, and reads the top-level
  key only for the legacy kanban opt-in.
- Disabled platforms are read with the runtime's plugins.disabled reader,
  so a non-list value (which the runtime ignores) is no longer reported
  as disabling a platform. Required credentials come from
  _platform_manifest_env_entries, which now takes optional=False.
- Tests use a synthetic platform manifest instead of the real Telegram
  one, and assert config check and migrate report the same names.
2026-09-29 20:34:45 +05:30
Gille
85b01aac9b fix(cli): diagnose stale toolsets and disabled platforms
(cherry picked from commit 98ab863de43995a133b2ccacdc790fb3589128ec)
2026-09-29 20:34:45 +05:30
ethernet
74163fa18c fix(desktop): pass release epoch to native version subprocess 2026-09-29 10:55:18 -04:00
Siddharth Balyan
5eb1381f39 fix(desktop): consecutive manage_connections calls share one card for everyone (#127872)
The tool-group slot folded consecutive manage_connections calls into one
connection card only when guided onboarding was enabled, while MCP targets
were folded for every user. Connector calls now fold the same way for
everyone, and the onboarding check is gone.
2026-09-29 19:29:29 +05:30
ethernet
ee5f49b943 fix(ci): give the stable-release reusable workflows their environment secrets
stable-release.yml called desktop-bundled-release.yml, docker.yml and
termux-verify.yml without `secrets: inherit`, so every job inside them that
declares `environment: release-signing` (or container-publish) attached its
deployment and read the environment's `vars`, but resolved `secrets.*` to the
empty string. The signed-candidate legs died at "Archive every pinned input"
with `missing env CLOUDFLARE_R2_ACCESS_KEY_ID`; the docker publish legs and
the termux packaging checks would have failed the same way.

A called workflow receives no secrets by default, and a job-level
`environment:` inside it is required but not sufficient: a caller job that
`uses:` a reusable workflow cannot declare `environment:` at all, so
`secrets: inherit` is the only mechanism that can carry them
(actions/runner#1490).

`secrets: inherit` is added only where the callee reads a protected
environment's secrets. ci.yaml, nix, pm-bundle, windows-venv-e2e and the
install-e2e chain reference none, and ci.yaml must stay secret-free by its own
header (it runs PR-controlled code).
2026-09-29 09:40:09 -04:00
Siddharth Balyan
fc042f1d67 refactor(desktop): route tool cards and bridge events through lookup tables (#127828)
* refactor(desktop): route tool cards through a table keyed on the card-tool names

The name-keyed if ladder in ChainToolFallback becomes TOOL_CARDS, typed
Record<CardToolName, FC>. CardToolName comes from the same list isCardTool
reads, so a card added to one list and not the other fails the build.
Behaviour is unchanged.

* refactor(desktop): dispatch desktop bridge events through a handler table

The six-branch event.type ladder becomes DESKTOP_BRIDGE_HANDLERS, one row
per event with its body and comments unchanged. Behaviour is unchanged.

* chore: retrigger CI (zero-job dispatch failure, auto-heal)
2026-09-29 18:37:11 +05:30
Siddharth Balyan
5eea87882a Clarify: one question shape and one result shape on every surface (skip, cancel, timeout and undelivered are told apart) (#127760)
* refactor(desktop): split clarify-tool.tsx into a clarify/ folder

Pure moves, no behaviour change. Parsing, the question-card core
(shell, choice rows, question block), the delivery watchdog, and each
pending/settled card get their own file so the core can be reused.

* feat(clarify): one questions[] shape with per-question status and one outcome

The clarify tool now takes only questions=[{question, choices?, multi_select?}].
A wrong shape is a tool error that names the right one, so a model that
sends the old top-level question/choices corrects itself on the next call.

Every result has the same shape on every surface: each response carries
status (answered, skipped, unanswered) with user_response null unless
answered, and the result carries one outcome (submitted, cancelled,
timed_out, undelivered) plus an optional surface-written notice. The
timeout and cancel sentences that used to pose as the user's answer are
gone, and the compressor reads status instead of matching their prefixes.

The callback contract is callback(questions) -> {answers, outcome, notice?}
(None = skipped, missing qid = unanswered). tui_gateway settles a batch the
same way for the last lock, a cancel, an interrupt and the deadline, and
clarify.lock keeps a null answer as a skip. A multi-select answer that is
not a JSON array counts as one typed ("Other") answer. The tool-row preview
reads the first question.

* feat(clarify): every surface asks through the one question card

CLI: the batch panel is the only panel; Enter on an empty field skips a
question, Ctrl+C cancels and keeps the locked answers, the deadline returns
timed_out. -q and -z return undelivered with a no-user notice.

Ink TUI: the single-question prompt is gone; the card supports multi-select
(Space or a digit toggles, Enter locks, typed Other text joins the picks),
an empty submit skips, Esc cancels, and "Batch" names are dropped.

Desktop: the single-question card is deleted and its keyboard handling moved
into the one card (arrows, letters and digits, auto-advance, Enter picks then
confirms). Confirm enables at one answer; blank questions lock null; Skip and
a composer message cancel; the settled card shows No answer for unanswered
questions. The bots room card follows the same contract.

Messaging: one card per question with a "Reply skip to skip" line in every
locale; timeouts and delivery failures map to timed_out and undelivered.

* fix(clarify): cancelled for a released messaging card, labels win over the skip word

A prose reply to a messaging choice card, /new and session end released
the wait with "", which read as timed_out. They now resolve with a
CANCELLED marker and the tool gets outcome cancelled.

A typed reply that matches a choice label ("Skip") now resolves to that
choice; the skip word applies only when no choice matches.

The bots room card sends the picked labels as they are; the tool already
strips the recommendation label. Unused OUTCOMES and a new docstring and
comment are removed.

* fix(tui): re-editing a multi-select answer keeps its picks

The Ink card stored a multi-select answer as display text ("A, B"), so
revisiting the question put the whole string into Other and re-locked it
as one item. The answers map now keeps the raw JSON array, the card
restores the picks on revisit, and only the display lines format it.

Comments that earlier edits reworded are restored to their original
words, minus the phrases the change made wrong. The compute-host clarify
lock accepts None for a skipped question.

* test(clarify): align three checks with the one-answer confirm and the regenerated keys

The desktop Cmd+Enter test now expects a send with one answer (the blank
question locks null), the compressor test expects the single-answer summary
the code gives, and locales/_keys.desktop.json is regenerated for the
removed and added clarify strings.

* test(tui): the one-question card header is singular
2026-09-29 18:33:44 +05:30
Siddharth Balyan
bc7f58f3b0 fix(gateway): first-contact note only in DMs, offer flag saved in the routed profile (#127818) 2026-09-29 12:59:23 +00:00
FroRaut
b8a8be185d fix(debug): mask URL tokens and passwords in debug share uploads
hermes debug share, gateway /debug, the dashboard share and the desktop
diagnostics upload sent credential query parameters (?token=, ?api_key=,
?access_token=) and user:pass@ userinfo from logged URLs unchanged, to
the paste service or to Nous storage. Log lines keep these values
because log-time redaction leaves web URLs alone, and Firecrawl, for
example, logs the full URL it scrapes at INFO.

The cause is hermes_cli/debug.py:_redact_log_text, the one redactor on
every share path. It called redact_sensitive_text(force=True) without
redact_url_credentials=True, so the strict URL pass that compaction,
memory context, cron errors and the kanban notifier already use never
ran on the upload copy.

Pass redact_url_credentials=True there. Parameter names, hosts and
paths stay readable; values of credential-named parameters and URL
userinfo become ***.

(cherry picked from commit f522171b836bfd842354f5a34c8c2bdd46b39467)
2026-09-29 07:51:44 -04:00
kshitijk4poor
5ed979bc30 test(gateway): kill live lock holders by captured identity, tolerate AccessDenied
Review follow-ups on the live strict-identity tests:

- keep the holder as the psutil.Process captured at spawn, so a PID
  recycled before class teardown is refused instead of killed;
- suppress psutil.Error (AccessDenied on an exiting Windows process),
  not only NoSuchProcess, so one failed kill cannot leave the rest of
  the tree alive and wedge communicate();
- bound the handshake at 20s (a normal start takes ~1s);
- garble the lock in binary mode like the restore does, and say why
  overwriting without truncating is safe;
- drop spawns_gateway_lookalike from the non-gateway holder test (its
  argv carries no `gateway run`) and note that the class holder spawns
  before the per-test guard is armed.
2026-09-29 17:15:05 +05:30
kshitijk4poor
e4e16a54ae test(gateway): bound and reap the live strict-identity lock holders
The live lock-holder tests could hang or leak:

- the handshake readline had no deadline and stderr went to DEVNULL,
  so a child stuck on import blocked the worker with no diagnostics;
- the holder slept 120s, outliving a killed pytest while holding
  gateway.lock;
- `assert acquire_gateway_runtime_lock()` vanishes under -O;
- kill() hit only the Windows venv launcher, never the self-reported
  interpreter, and pipes were never closed (ResourceWarning per test);
- the discovery test relied on list_profiles() resolving to the temp
  HERMES_HOME and compared unresolved paths (/var vs /private/var on
  macOS, 8.3 names on Windows).

The holder now blocks on stdin, reports failure via exit + captured
stderr, and teardown kills the launcher tree plus the reported PID and
reaps via communicate(). The four class tests share one class-scoped
holder with per-test restore of gateway.pid / gateway.lock (5 spawns ->
2), and discovery pins the profile roots like the named-pipe live test.

Also read gateway.pid with utf-8-sig (windows-footguns lane).
2026-09-29 17:15:05 +05:30
emozilla
5d1ab29b03 fix(gateway): identify a live gateway from its lock when gateway.pid is gone
hermes update on Windows maps running gateways to profiles through
get_running_pid_identity_strict, which raised "active gateway lock has no
PID metadata" whenever the lock was held but gateway.pid was missing. The
whole update aborted before touching anything.

That state is reachable on Windows: status cleanup unlinks gateway.pid and
gateway.lock together, and the lock unlink fails silently while the live
gateway holds the file open, so only gateway.pid disappears (#96582).

The gateway writes the same identity record into the lock when it acquires
it. The strict probe now takes the PID from the lock record, requires a
PID file that exists to agree with it, and runs every existing liveness,
start-time and command-line check against what is present. A garbled lock,
a disagreeing PID file, or a lock holder that is not a gateway still aborts.

(cherry picked from commit 1d6bfbbf94605fcc4c84cb52636887e1e8ae36c1)
2026-09-29 17:15:05 +05:30
kshitijk4poor
d9f6a844c1 refactor(update): print the owed-tail notice once, in the process that boots
A supervised start that also relaunches onto the store interpreter printed the
notice in both processes; it now prints only after the relaunch decision.
The _supervised_child docstring claimed the neighbouring truthy set, which it
does not match, and prepare_launch said a supervised child "never" retries the
tail although stale dependencies still do; both reworded. The supervised test
also pins a clean stdout and a single notice.
2026-09-29 17:05:00 +05:30
kshitijk4poor
a91a24ccdf fix(update): a supervised start says when it leaves the update tail owed
The supervised exemption keeps the pending marker for `hermes update`, but
nothing reported it: a headless host running only the gateway would keep
the unfinished tail (products, skills sync, config migration) indefinitely.
One stderr line per start lands in the unit's journal; stdout stays clean
for commands that emit machine-readable output.
2026-09-29 17:05:00 +05:30
kshitijk4poor
fcf1ea4b99 test(update): pin the supervised-child exemption by behaviour, two tests
Fold the marker off-value table into the supervised-launch test so 0/false
are proven to still repay the tail through prepare_launch, instead of a
separate test of the private parser. Keeps the stale-dependency sync test.
2026-09-29 17:05:00 +05:30
liuhao1024
69f8b703fc fix(update): narrow the supervised-child exemption to the sticky tail
Review follow-up: the early-bail also skipped the one-shot stale-dependency
sync, so a supervised child on an out-of-date tree booted in place (or
crash-looped under Restart=always) instead of syncing and relaunching.
Gate only the pending-tail half, and parse the markers with the repo's
truthy set so 0/false do not suppress anything.

(cherry picked from commit 0838ce2a916faee5fe7f6a6fa30db3d0a6146647)
2026-09-29 17:05:00 +05:30
liuhao1024
8d22a824ff fix(update): keep supervised launches off the pending completion tail
A supervised child (systemd unit, launchd plist, Windows task, s6 — all
export HERMES_SUPERVISED_CHILD or HERMES_S6_SUPERVISED_CHILD) is
relaunched on every manager start. prepare_launch() treats the
source-completion-pending marker as a debt to repay immediately, so a
sticky marker re-runs the completion tail (and its environment builds)
on every boot until the disk fills (#123340). Skip the launch-time
repair for launcher-marked children; the tail stays the CLI's to finish
via hermes update / hermes pm install. INVOCATION_ID is deliberately
not read: systemd exports it to every descendant, so an ordinary hermes
command inside a CI runner would skip a legitimate repair.

(cherry picked from commit 4203c068b8b2df5f6b74b4ea610de15bfa2ef54a)
2026-09-29 17:05:00 +05:30
kshitijk4poor
914d4fcaf5 test(update): pin the handoff sentinel to the real current entrypoints
The sentinel local is never read, so a well-meaning unused-variable
cleanup could delete it and silently re-arm the mid-update takeover.
Assert it in co_varnames of update_cmd._cmd_update_impl and
main.cmd_update (unwrapping update_receipt_scope).
2026-09-29 16:47:48 +05:30
kshitijk4poor
db17e07389 fix(update): mark current updater frames with a sentinel, not missing locals
The pre_update_version co_varnames check added in af26acab73 rejected the
current updater's entrypoints, but it also rejected historical updaters
from 2026-07-26..08-16, whose _cmd_update_impl/cmd_update never declared
that local — their in-process dependency installs would have continued on
the old interpreter instead of handing off to the takeover child.

Invert the gate: the CURRENT updater declares the
_hermes_current_updater_frame sentinel local in both entrypoints; no
historical on-disk version can contain it. in_historical_update() now
hands off on entrypoint match unless the sentinel is present, restoring
the handoff for every historical window while still refusing current
frames. Verified live for all four frame classes; mutation check: with
the sentinel check removed, the current-updater refusal test fails.
2026-09-29 16:47:48 +05:30
kshitijk4poor
09581cacaa refactor(desktop): gate the HUD bridge sentence on tool availability, not note text
Pick the tools the bridge sentence names from the same availability
conditions that gate the note's sentences instead of substring-searching
the rendered prose, and assert the bridge clause by tool membership
rather than exact wording (simplify-code quality review).
2026-09-29 16:40:36 +05:30
kshitijk4poor
312f6bc2fd fix(desktop): HUD note fires again when tool search defers the desktop tools
Since #97979 put the desktop GUI tools on the default tool_search defer
list, a Desktop session's valid_tool_names no longer contains
read_window_below (it sits behind the tool_call bridge), so
hud_surface_note() withheld the whole note on every HUD turn: the model
stopped being told that "this"/"here" means the app behind the HUD.

hud_surface_note() now takes the session's deferred-but-callable names
(the same agent/tool_executor._tool_search_scoped_names set the tool_call
dispatch validates against) and counts them as available, adding one
sentence telling the model to call the deferred ones through the
tool_call bridge — a direct call to a deferred name is rejected as an
unknown tool. The defer list, its token saving and the note text for
non-deferred sessions are unchanged.
2026-09-29 16:40:36 +05:30
hermes-seaeye[bot]
6ffe3b2b59 fmt(js): npm run fix on merge (#127730)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-09-29 10:43:40 +00:00
kshitijk4poor
a042567296 test: tighten the audit's new assertions after review
- relay-deliver-budget: the ceiling/timeout composition is checked
  against the imported constants (which the mirror tests already tie to
  the Python defaults) instead of re-reading config_defaults.py.
- terminal env sync: the key recorder also traps get()/[] and is an
  instance, the probe fails loudly if the bridge looks up no keys (so the
  gateway-only-key check cannot go vacuous), one _cli_env_map() helper
  serves both tests, and the docstring says what is actually driven.
- live_system_guard bypass reuses _live_system_guard_is_active().
- retaindb no-client prefetch asserts no thread is spawned through the
  module's spawn_context_thread seam, not a private list.

Each changed assertion re-proven red by its production mutation.
2026-09-29 15:58:55 +05:30
kshitijk4poor
34a5f95a30 test: restore five contracts the preservation review found uncovered
An independent review of every deleted test against its named keeper
found contracts whose only proof had gone, or that no test had ever
pinned. Each restoration is one assertion in an existing test, proven red
by the production mutation named:

- gateway /model reply shows the context length the async resolver
  returns (the deleted async-matches-sync test was its only check);
  red when resolve_display_context_length_async drops the value.
- parse_model_switch_args("sonnet") has scope "default" (the deleted
  tuple test covered the no-flag case).
- shared metrics runtime sends nothing when collection is on but send is
  off; red when the gate reads resolved.enabled instead of resolved.send.
- the terminal env-sync test also probes every key gateway.run's bridge
  looks up, so a gateway-only key (which the old source parse caught)
  still fails it.
- desktop: the deliver ceiling equals the backend's lock wait plus every
  attempt, read from the Python defaults.

Also: the tools/bot_relay.py mirror comment names relay-budget.ts, and a
comment orphaned by the Windows encoding-test deletion goes with it.
2026-09-29 15:58:55 +05:30
kshitijk4poor
7e6b8d9ec8 test(desktop): pin the bot_relay.deliver budget by import, not by regexing relay.ts
relay-deliver-budget.test.ts read relay.ts as text: it regexed each
constant ("must stay a literal so this test can read it") and matched the
deliver call shape within 400 characters of 'bot_relay.deliver', which
AGENTS.md bans. The regex passes with the identifier wired into the wrong
argument and fails on a correct refactor.

The #93911 budget constants move to relay-budget.ts and are imported by
relay.ts and both tests. The mirror checks against the Python defaults are
unchanged. The call-site check becomes a behavioural one in relay.test.ts:
the mocked requestProfile for bot_relay.deliver must receive
RELAY_DELIVER_TIMEOUT_MS (red when the call passes any other deadline).
2026-09-29 15:58:55 +05:30
kshitijk4poor
93a7267b10 test: fold private-predicate and replayed-contract tests into their keepers
Each removed test re-asserts a contract a stronger test already owns at
the real boundary; the keeper is named per group.

- api_server CORS: `_origin_allowed` for an allowlisted origin and for an
  Origin-less client -> the configured-origin preflight test and every
  Origin-less /health request. The Idempotency-Key allow-header assertion
  moves into the preflight test instead of running a second identical
  preflight.
- api_server room grants: the execution-policy predicate's two outcomes ->
  test_api_server_runs' HTTP grant-refresh tests (403 on drift, 200 on the
  authorized digest).
- api_server multimodal: the pure input_image canonicalization -> the
  /v1/responses test that sends the same parts and asserts the same shape.
- credential pool: matched-hint rotation (401) and second-429 rotation ->
  test_credential_pool_routing's real-pool hint test and TestPoolRotationCycle,
  which assert the same steps plus the exact rotate kwargs and swap.
- shared metrics: two runtime cases that replay the send resolver's truth
  table -> TestSendRequiresCollection; the e2e gzip-retry test (both
  attempts compress in the same second, so it cannot see mtime) ->
  TestCompression::test_gzip_is_deterministic_across_time; e2e outbox
  untouched -> TestOutboxIsNotAQueue.
- mcp oauth: provider reuse across reconnects -> the manager restore test;
  the non-tty private predicate -> build_oauth_auth's fail-fast test; the
  manager disk-watch test -> the integration test, which gains its one
  unique assertion (an unchanged file returns False).
- startup watchdog: disarm-without-arm -> test_disarm_is_idempotent, whose
  second disarm runs in the same state and branch.
2026-09-29 15:58:55 +05:30
kshitijk4poor
b65972c1a5 test: give four vacuous tests an assertion that can fail
Each test passed with the behaviour it names removed (proven by mutating
the production owner on the old and new test):

- s6 hooks test_register_noop_on_host: the hook swallows the
  AssertionError into a printed warning, so reaching the manager never
  failed the test. Assert nothing was printed, as the sibling test does.
- live_system_guard bypass: signal 0 to our own PID passes the guard too.
  Assert os.kill is the unpatched builtin.
- retaindb queue_prefetch without a client: a missing guard fails only in
  background threads. Assert no prefetch thread was started.
- test_config golden migration parity: drop the `_config_version == 33`
  exact-match branch, which cannot run again now the version is 49; the
  subset check it fell through to is unchanged.

test_terminal_config_env_sync now drives gateway.run's
_bridge_terminal_config_to_env with every known terminal key instead of
parsing gateway/run.py's source for a dict literal. It asserts each key
reaches the same TERMINAL_* var as the CLI, which the key-set comparison
could not check, and removes the last inspect.getsource in tests/.
2026-09-29 15:58:55 +05:30
kshitijk4poor
1ad403f508 refactor(shared_metrics): remove reset_warning_latch_for_tests
A test-only export with one autouse fixture as its only caller. No test
asserts the warn-once line the latch guards, so resetting it protects
nothing.
2026-09-29 15:58:55 +05:30
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
9ee791eef1 test: drop tests that cannot fail or duplicate a same-file keeper
Each removed test either asserts nothing reachable or repeats another
test's exact body:

- auxiliary_client empty-base_url: body only writes config.yaml; a merge
  (890bbbda1f) restored the header of a test b3d4f67b20 had purged. The
  non-Anthropic-host test beside it drives the same default fallback.
- background_review no-session-db: _record_review_usage_to_parent swallows
  every exception and there is no DB to observe, so removing the guard
  still passes; the path has no observable effect to pin.
- code_execution Windows "would have failed" pair: every branch returns,
  skips or passes; they exercise CPython defaults, not Hermes code.
- cron test_should_use_direct_api_call_*: byte-identical copy of the test in
  tests/agent/test_cron_inline_api_call.py (0213b8b77b had to edit both).
- cron liveness no-multiplexer: identical body to test_no_lock_no_pids_is_false;
  its multiplexer patch is never reached.
- browser console/get_images "private URLs allowed": identical to the
  local-backend test in each file; both mock the predicate the name promises.
- hermes_state TestTitleUniqueness: the duplicate-title case is a strict
  subset of TestSessionTitleLineage::test_unrelated_session_still_conflicts;
  the NULL-title case never writes a title.
2026-09-29 15:58:55 +05:30
kshitijk4poor
bca2e3c5a4 docs(profiles): say the interactive uninstall menu still offers a full wipe
The stale-profile recovery paragraph claimed uninstall only runs in a
mode that keeps user data. Plain interactive `hermes uninstall` is let
through and its menu still offers Full uninstall of the default root,
behind its confirmation prompts. Say so, and note in the predicate why
--full is refused (it wipes without asking) while the menu is not.
2026-09-29 15:45:52 +05:30
kshitijk4poor
192e574efe docs(profiles): what still runs when the sticky profile was deleted 2026-09-29 15:45:52 +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
JoaoMarcos44
205d5d78f1 fix(debug): strict URL-credential redaction for the system dump
A fallback_providers base_url can carry its credential in the URL (userinfo, ?key=, #access_token=, X-Amz-Signature=). The force pass leaves those to the log policy, which spares OAuth/magic-link URLs, so hermes dump printed them and debug share / gateway /debug uploaded them. The dump's fallback rendering and the upload copy of the dump now opt into redact_url_credentials.

The strict canonicalizer also turned x-amz-signature into x_amz_signature before the policy lookup, so strict mode never masked it; hyphenated policy names now match as stored.

Ported from #126168.
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
ea7c6a1339 refactor(update): one explanation of the tail exemption, with the lock guard reworded
The lock-ancestry comment still claimed it was what stops a tail recursing;
the tail scripts now return before it, so it describes the processes an
update spawns instead. Also note why the identity reads sys.argv[0] rather
than the argv parameter.
2026-09-29 15:25:13 +05:30
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