Skip to content

Upstream sync — merge Hermes upstream/main (3,189 commits)#16

Open
tomi204 wants to merge 3190 commits into
mainfrom
sync/upstream-20260713
Open

Upstream sync — merge Hermes upstream/main (3,189 commits)#16
tomi204 wants to merge 3190 commits into
mainfrom
sync/upstream-20260713

Conversation

@tomi204

@tomi204 tomi204 commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Upstream sync — 3,189 commits

Merges upstream/main (NousResearch/hermes-agent) into the ClawPump fork. 2,985 files, +392k/-56k. Real merge commit (2 parents) so the shared merge-base is preserved — merge with a merge commit, do not squash.

Conflicts resolved (33)

"Keep ClawPump customizations, fold in upstream." Resolved via a fan-out of one reviewer per file, then hand-verified. Highlights:

  • Electron .cjs → .ts refactor (the big structural one): upstream exploded the electron layer into ~30 .ts modules. Re-threaded ClawPump's bootstrap / self-update fork-pin (ensureClawpumpOrigin — verified present at both update entry points), dashboard-token, and hardening into the new structure. Fixed 4 stale dashboardArgs → backendArgs refs that would've thrown at runtime.
  • Money/auth (auth.py, model_setup_flows.py): kept ClawPump usepod branches alongside upstream's new copilot / zai branches.
  • UD files (upstream deleted, we'd only rebranded): accepted upstream's refactor, re-applied "Claw Agent" strings to new homes (backend-ready.ts, use-message-stream/*, thread/status.tsx).
  • apps/desktop version → 0.17.0 (upstream), ClawPump branding kept.

Dropped-feature regressions the build gates caught & I fixed

The autonomous resolution silently dropped several ClawPump additions; each was caught by tsc/tests and restored:

  1. Composer $pendingChatPrompt drain — the wallet/x402/mail "use in chat" prefill. Re-homed into use-composer-draft.ts.
  2. Sparkles icon export — broke the MCP page. Restored.
  3. ChatSidebar tools panel + ToolCall.tsx (whole feature spanned both; ToolCall.tsx had been staged for deletion). Restored.
  4. PodSetupDialog — upstream @nous-research/ui Button variantoutlined.

Verification (all green before commit)

Gate Result
ClawPump test suite 460 passed
Python compile + imports
Desktop tsc -b + full build (renderer + electron main.mjs)
Web tsc -b + build
Conflict markers / unmerged paths ✅ 0 / 0
Major ClawPump features present ✅ web+desktop routes/nav, usepod, fork-pin, install re-point, agent-mail

⚠️ Reviewer follow-ups (non-blocking)

  • scripts/install.ps1 — brace-balanced but not PowerShell-parsed locally (no pwsh). Parse before release.
  • Backdrop claw watermark — upstream replaced Backdrop.tsx with a backdrop plugin slot; watermark not yet re-added (deferred, cosmetic).
  • i18n "Hermes Cloud" strings left untranslated (they name the external Nous service).
  • ChatSidebar tools panel kept per "preserve ClawPump features"; confirm you still want it (upstream removed its own web tools UI).

🤖 Generated with Claude Code

PRATHAMESH75 and others added 30 commits July 11, 2026 11:07
Keep empty-tail recovery scoped to the current stream segment and bound fallback flood retries. Preserve Telegram's server retry hint without blocking final delivery through a long cooldown.
…ions

Copilot (api.githubcopilot.com/responses) binds replayed assistant
codex_message_items ids to a specific backend "connection". Credential-
pool rotation, a gateway restart, or routine load-balancer churn between
turns all invalidate that binding, and Copilot rejects the stale id with
HTTP 401 "input item ID does not belong to this connection" — even for
short ids well under the #27038 64-char length cap, since this is a
connection-scope problem, not a length problem. Once a session captures
one of these ids it is persisted and replayed forever, permanently
bricking the session.

Thread an is_github_responses flag from build_kwargs/convert_messages
into _chat_messages_to_responses_input and drop the id unconditionally
on that path, mirroring how reasoning items already strip id on replay.
phase/status/content are still replayed so cache-relevant signal isn't
lost — only the connection-scoped id is unsafe to reuse.

Written to apply independently of the #27038 length-cap fix so the two
PRs don't block each other; they touch adjacent conditions in the same
block and merge cleanly in either order.

Fixes #32716

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
_CodexCompletionsAdapter (agent/auxiliary_client.py) is a second,
independent producer of Codex Responses input — used by auxiliary
calls (context compression, flush_memories, MoA aggregation,
session_search) that route through CodexAuxiliaryClient instead of
the main agent's ResponsesApiTransport.build_kwargs. It calls
_chat_messages_to_responses_input() directly without is_github_responses,
so the previous commit's fix didn't cover it: an auxiliary call made
against a Copilot-backed session could still replay a connection-scoped
codex_message_items id and hit the same HTTP 401.

Detect the Copilot host from the adapter's own client.base_url (same
check the adapter already does further down for prompt_cache_key
opt-out) and pass is_github_responses through, closing the gap.

Still #32716.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Require literal booleans for backend-specific replay policy and pin
non-default status and content preservation through both response paths.
Reapply the endpoint-aware preflight after request and execution
middleware so no override can reintroduce a connection-scoped ID.
Exercise request and execution middleware replacements through the real
conversation loop and assert the provider payload is sanitized.
… in fetch_models

fetch_models() sends Authorization: Bearer <api_key> plus any
default_headers (x-api-key etc.) via urllib.request.urlopen, and
urllib's redirect handler forwards every header when following a
3xx — including to a different host. A catalog endpoint (or a
compromised/misconfigured proxy in front of it) answering with a
redirect to another origin therefore received the provider API key.

Install an HTTPRedirectHandler that drops authorization, x-api-key,
api-key, x-goog-api-key and cookie when the redirect target hostname
differs from the original request, mirroring the pattern already used
in skills/creative/comfyui/scripts/_common.py. Same-host redirects
keep credentials so legitimate path-level redirects still work.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…g credentials

Review feedback: a same-host redirect to a different port can land on a
different service, which must not inherit the provider API key. Compare
(scheme, hostname, effective port) — with 80/443 defaults — instead of
hostname alone, and add a two-server regression test for the
same-host/different-port case.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
`hermes -t web chat` silently dropped the toolset filter (and the same
hold true for `-m`, `--provider`, `--tui`, `--dev` placed before
`chat`). Reported in #28780 for `-t/--toolsets`; the others are sibling
failures with the same root cause.

Root cause: the chat subparser re-declared these flags with `default=None`
(or `default=False` for store_true) on top of the matching top-level
parser flags. When argparse dispatches into the subparser it shares the
namespace via `dest`, so the subparser's default overwrites whatever the
top-level parser parsed before the subcommand. `-s/--skills`, `-r/-c/-w`,
`--yolo`, and `--pass-session-id` already use `default=argparse.SUPPRESS`
for exactly this reason — the chat-subparser action becomes a no-op
unless the user explicitly passes the flag after `chat`, and the parent
value survives.

Reproduction (origin/main, before fix):

  >>> parser.parse_known_args(["-t", "web", "chat"]).toolsets
  None
  >>> parser.parse_known_args(["chat", "-t", "web"]).toolsets
  'web'

After fix:

  >>> parser.parse_known_args(["-t", "web", "chat"]).toolsets
  'web'
  >>> parser.parse_known_args(["chat", "-t", "web"]).toolsets
  'web'

Sibling flags fixed in the same commit because they share the exact same
argparse pattern bug — verified via a new contract test that scans every
chat-subparser action whose `dest` is also on the top-level parser and
asserts `default is argparse.SUPPRESS`. The test fails on origin/main
listing all five offenders and passes after this fix.

Test additions in tests/hermes_cli/test_argparse_flag_propagation.py:
- TestChatSubparserInheritedValueFlags exercising real `_parser` build
  (not the hand-rolled replica) so it catches future drift.
- Parametrized before-chat / after-chat cases for `-t`, `--toolsets`,
  `-m`, `--model`, `--provider`.
- Negative case: passing none of the flags leaves attrs at the top-level
  parser's `None` default (SUPPRESS does not remove existing attrs).
- Combined case: all three value flags before `chat` simultaneously.
- store_true cases for `--tui` / `--dev`.
- Contract test asserting every shared-`dest` flag on chat uses SUPPRESS.

Fixes #28780.
* fix(agent): persist truthful tool effect dispositions

* fix(agent): preserve successful siblings during orphan recovery

* fix(agent): narrow effect dispositions to none and unknown
ethernet8023 and others added 29 commits July 13, 2026 17:22
The set-matrix step wrote the npm workspace query result directly to
$GITHUB_OUTPUT. If discovery ever produced [], the matrix would expand
to zero check jobs, leaving the reusable workflow green without running
any JS/TS checks.

Now the step validates the result is a non-empty array before emitting
it, and exits 1 with a GitHub annotation if it's empty or jq failed.
…lassifier

classifyNativeBinary only checked big-endian Mach-O/Fat magic bytes
(feedfacf, feedface, cafebabe). Real Darwin .node files from node-pty
prebuilds are stored little-endian on disk (cffaedfe = MH_CIGAM_64),
so every Darwin prebuild classified as null, and validateStagedBinaries
threw a platform mismatch on macOS — breaking npm run check for every
macOS contributor. CI didn't catch it because runners are Linux (ELF
path was correct) and tests only planted big-endian fake headers.

Add recognition for all six Mach-O/Fat byte orderings:
- MH_CIGAM (cefaedfe) — LE 32-bit
- MH_CIGAM_64 (cffaedfe) — LE 64-bit [the one real prebuilds use]
- FAT_CIGAM (bebafeca) — LE universal

Update makeFakeNode to write LE CIGAM_64 bytes for the darwin fixture
(matching real on-disk format) and add regression tests for all new
magic forms.
…in-bash

fix(windows): survive broken Git Bash login shells
Prevent credential forwarding across catalog redirects, retain explicit opt-in semantics for paid media backends, fail closed on invalid provider configuration, avoid mixed-catalog and output-limit assumptions, and reserve native STT provider names.
Forward explicit catalog refreshes and make the TTS availability gate follow the configured provider instead of unrelated credentials.
alelpoan's fix (emit hermes:zoom:changed after restore) is correct, but the
bug's root is duplication: setAndPersistZoomLevel and restorePersistedZoomLevel
each independently did setZoomLevel + send, and restore forgot the send.

Collapse both (and the lifecycle re-assert) into a single applyZoomLevel()
helper in zoom.ts that always applies-then-notifies — the regression can't
recur by omitting a send. Replace the source-grep test (which broke on main:
the sibling source-assertion pet test it copied was refactored to a behavioral
one, dropping the fs/path imports it relied on) with behavioral coverage of the
funnel, matching zoom.ts's "unit-testable without booting a BrowserWindow"
convention.

Co-authored-by: alelpoan <alelpoan@proton.me>
…tore-sync

fix(desktop): sync UI Scale control after zoom restore (supersedes #63842)
Skipped jobs (conclusion == 'skipped') have null/zero-duration timestamps
that polluted every downstream computation: they counted as 'unchanged'
(0 vs 0) in faster/slower tallies, showed meaningless '0.0s (0%)' deltas
in the job table, rendered phantom gantt bars, and inflated wall/compute
totals.

Add is_skipped() helper and apply it consistently:
- compute_stats: exclude from wall/compute + faster/slower/unchanged;
  add 'skipped' and 'bl_skipped' counts
- _gantt_bars: filter from current bars, baseline bars, and axis calc
- _job_table: show 'skipped' label instead of durations/deltas
- _step_details: skip entirely (no meaningful step data)
- _regressions: exclude from both current and baseline sides
Add wait time computation: for each job, wait_s = started_at - max(completed_at
of all jobs that finished before it started). This is a timestamp heuristic
(no workflow YAML dependency parse needed) that's accurate for pipeline-shaped
CI where the critical path is linear at each stage.

Shows up in:
- Job table: new Wait + Δ Wait columns
- Gantt chart: hatched bar segment before the run bar
- Step details: '(wait Xs)' annotation in the summary line
- Markdown summary: Total wait row with delta vs baseline
- Stats: total_wait / bl_total_wait in compute_stats

Also completes the skipped-jobs UI:
- Skipped stat card in stats cards
- Skipped row in markdown summary table

Backward compatible: old cached baselines without wait_s annotate on load.
Baseline bars were positioned at their absolute timeline offset, which
included the baseline run's own wait time — making duration comparisons
hard since the bars were visually offset. Now each baseline bar starts
at the same left position as its corresponding current job, so the two
bars directly overlap for at-a-glance duration comparison.

Also removed the now-unused bl_t0 / bl_max / bl_jobs_timed variables
that tracked the baseline timeline position.
run_job() constructs SessionDB() synchronously with no timeout of its
own, unlike the agent's run_conversation call further down, which is
already bounded by HERMES_CRON_TIMEOUT. A wedged sqlite3.connect (e.g.
a stale flock from a crashed sibling process) hangs this call
indefinitely.

That hang is invisible to every existing cron safeguard because it
happens before _submit_with_guard's future exists: the finally block
that discards the job ID from _running_job_ids never runs. The job
stays wedged "running" — every later tick logs "already running —
skipping" — until the whole gateway process is restarted.

Observed in production: a cron job's worker thread was confirmed via
a live py-spy thread dump to be parked inside SessionDB.__init__'s
sqlite3.connect for 3+ days, silently skipping every scheduled fire
in between across a gateway process that otherwise stayed healthy.

Bound the SessionDB() construction with its own timeout
(HERMES_CRON_SESSION_DB_TIMEOUT, default 10s), following the same
bounded-thread-pool pattern already used elsewhere in this file (the
delivery retry path, and the agent inactivity watchdog just below).
On timeout, log at ERROR and proceed with session_db=None instead of
degrading silently to debug level, since an actual hang here is a new
condition worth surfacing.

Adds tests/cron/test_sessiondb_init_hang.py, including an end-to-end
regression proving the dispatch guard is released and a subsequent
tick can fire the same job again after a simulated hang.
Salvage of #63935. The original fix read HERMES_CRON_SESSION_DB_TIMEOUT
from a bare env var, but AGENTS.md requires non-secret behavioral
settings to live in config.yaml with an env var bridge only for
backward compatibility.

Changes:
- Add cron.session_db_timeout_seconds to DEFAULT_CONFIG (default 10s)
- Resolution order: HERMES_CRON_SESSION_DB_TIMEOUT env override →
  cron.session_db_timeout_seconds in config.yaml → 10s default
  (mirrors the existing script_timeout_seconds pattern)
- 0 = unlimited (opt-in for debugging, skips the bound)
- Strengthen test: assert the warning is logged on invalid env value
  (caplog was taken but never asserted)
- Add test: verify config.yaml resolution path works end-to-end

Co-authored-by: LoicHmh <26006141+LoicHmh@users.noreply.github.com>
The _PROVIDER_PREFIXES frozenset in agent/model_metadata.py is static
and does not auto-extend from ProviderProfile. Removing deepinfra and
deep-infra from it broke provider:model prefix stripping for DeepInfra.
`_module_registers_tools()` reads each `tools/*.py` file and fully
AST-parses it to check for a top-level `registry.register()` call.
90 files are scanned on every process start — but only 32 actually
register tools.

Add a cheap text prefilter: after reading the file (which we need to
do anyway for AST), check that both `"registry"` and `"register"`
appear in the source before calling `ast.parse`. A file with a
top-level `registry.register()` call must contain both strings, so
this is a perfect superset — zero false negatives. 50 of 90 files
skip the AST parse entirely.

The `source=` parameter is not threaded through `discover_builtin_tools`;
the prefilter lives entirely inside `_module_registers_tools`, keeping
the public API unchanged.

Benchmark (median of 10 runs, scanning 90 files):

  before (read + ast.parse all):  305.9ms
  after  (text prefilter + ast):   187.8ms
  speedup: 1.6x  (118ms saved)

Identical module set: 32 modules, same names, same order.
… (#55546)

The retry loop computed safe_out from the error's available_tokens,
which reflected the *previous* request. Between retries the agent
appends tool results and error text, so the real input token count
grows. Deriving safe_out from the stale budget meant every retry
still exceeded the context ceiling by 1+ tokens, burning through the
3-attempt limit.

Compute safe_out from estimate_messages_tokens_rough(messages) so
the cap tracks the growing input on each retry attempt.
…ry cap

The branch computed safe_out from estimate_messages_tokens_rough(messages),
but the provider rejected the larger api_messages request (system prompt,
injected context, tool schemas). When API-only content is large, safe_out
could far exceed the provider's available_tokens.

Compute safe_out from estimate_request_tokens_rough(api_messages, tools=...)
and keep provider available_out as an upper bound. Do not alter context_length
or trigger compression for output-cap errors.

Add production-path run_conversation tests that assert the retry API call's
max_tokens, including a case where a large system prompt makes messages-only
estimation undercount the real request.

Fixes #55546
…mpression-disabled guard

Salvage of #63862. is_output_cap_error() returns False for vLLM/LM Studio
error messages that contain 'prompt contains ... input tokens' (treated as
input-overflow signal). But parse_available_output_tokens_from_error() CAN
extract a valid available_tokens from those same messages. The
compression-disabled guard only checked is_output_cap_error(), so vLLM/LM
Studio users with compression off still got a terminal failure instead of
the max-tokens retry.

Fix: also exempt when parse_available_output_tokens_from_error() returns a
value — that function determines whether the retry path can actually handle
the error, so it's the right predicate for the exemption.

Added test: verify vLLM-format error with compression_disabled=False still
triggers the max-tokens retry path.

Co-authored-by: dmabry <dmabry@users.noreply.github.com>
#63955 made Hermes survive a broken `bash -l` (Ainz's `Directory
\drivers\etc does not exist`) by falling back to non-login `bash -c`.
But a non-login shell never sources /etc/profile, so it never gets
`…\usr\bin` on PATH — and that dir holds every coreutil the file/terminal
tools shell out to (cat, mktemp, mv, wc, head, stat, chmod, mkdir, find).
Result: `write_file` returned bytes_written:0 with an EMPTY error (the
failure text went to a missing binary's stderr) and terminal commands
exited 127. The survive-broken-login-bash fix was only half-done: it
stopped crashing but silently failed every write.

Derive Git Bash's bin dirs (mingw64/bin, usr/bin, bin, …) from the
resolved bash.exe and prepend them to the subprocess PATH on Windows, in
/etc/profile precedence order so coreutils win over same-named System32
tools (find.exe, sort.exe) inside the shell. No-op off Windows and when a
login snapshot is healthy (the snapshot re-exports the full PATH inside
the shell), so this only bites on the broken-login fallback path.

Adds _git_bash_bin_dirs() (derivation, cached) + _prepend_git_bash_dirs()
(PATH merge), plus regression tests for PortableGit/MinGit layouts and
the run-env injection ordering.
…oreutils-path

fix(windows): put Git Bash coreutils on PATH for the non-login fallback
Syncs 3,189 upstream Hermes commits into the fork. 33 conflicts resolved with
the "keep ClawPump customizations, fold in upstream" philosophy.

Notable resolutions:
- Electron layer .cjs->.ts refactor: re-threaded ClawPump bootstrap / self-update
  fork-pin (ensureClawpumpOrigin), dashboard-token, and hardening into upstream's
  new .ts modules; fixed 4 stale dashboardArgs->backendArgs refs that would have
  thrown at runtime.
- auth.py / model_setup_flows.py: kept ClawPump usepod branches alongside
  upstream's new copilot / zai branches.
- Restored ClawPump features the initial resolution dropped (caught by the
  build/type gates): composer $pendingChatPrompt drain (the wallet/x402/mail
  "use in chat" prefill), the Sparkles icon export, and the ChatSidebar tools
  panel + web/src/components/ToolCall.tsx.
- PodSetupDialog: upstream @nous-research/ui Button API change (variant ->
  outlined).
- Upstream-deleted, ClawPump-rebranded files (backend-ready.cjs,
  use-message-stream.ts, thread.tsx, Backdrop.tsx): accepted upstream's refactor
  and re-applied the "Claw Agent" strings to their new homes (backend-ready.ts,
  use-message-stream/{gateway-event,index}.ts, thread/status.tsx).
- apps/desktop version -> 0.17.0 (upstream), ClawPump branding retained.

Verified before this commit: 460 ClawPump tests pass; web + desktop tsc -b and
full builds clean; Python compiles and imports.

Flagged for reviewer:
- scripts/install.ps1 is brace-balanced but was not PowerShell-parsed locally
  (no pwsh) - parse before release.
- Backdrop claw watermark not yet re-added; upstream replaced Backdrop.tsx with a
  `backdrop` plugin slot - re-add as a follow-up.
- i18n "Hermes Cloud" strings left untranslated (they name the external Nous
  service); rebrand later if desired.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-advanced-security

Copy link
Copy Markdown

You are seeing this message because GitHub Code Scanning has recently been set up for this repository, or this pull request contains the workflow file for the Code Scanning tool.

What Enabling Code Scanning Means:

  • The 'Security' tab will display more code scanning analysis results (e.g., for the default branch).
  • Depending on your configuration and choice of analysis tool, future pull requests will be annotated with code scanning analysis results.
  • You will be able to see the analysis results for the pull request's branch on this overview once the scans have completed and the checks have passed.

For more information about GitHub Code Scanning, check out the documentation.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.