fix(nix): drop stale "vercel" group from #full variant - #33773
Merged
ethernet8023 merged 1 commit intoMay 29, 2026
Merged
Conversation
The `vercel` optional-dependency was removed from pyproject.toml in NousResearch#33067, but `nix/packages.nix` (added a few hours later in NousResearch#33108) still references `"vercel"` in the `#full` variant's `extraDependencyGroups`. uv2nix fails evaluation with: error: Extra/group name 'vercel' does not match either extra or dependency group Because `nix/devShell.nix` does `inputsFrom = builtins.attrValues self'.packages`, the broken `#full` derivation is pulled into the dev shell too, so `nix develop` / direnv breaks on a fresh clone — not just `nix build .#full`.
7 tasks
ethernet8023
enabled auto-merge
May 29, 2026 14:11
ethernet8023
added a commit
that referenced
this pull request
May 29, 2026
will catch bugs like #33773 before they merge!
19 tasks
ethernet8023
disabled auto-merge
May 29, 2026 15:16
3 tasks
alt-glitch
pushed a commit
that referenced
this pull request
Jun 14, 2026
…rcel-group fix(nix): drop stale "vercel" group from #full variant
T02200059
pushed a commit
to T02200059/hermes-agent
that referenced
this pull request
Jun 18, 2026
…rop-stale-vercel-group fix(nix): drop stale "vercel" group from #full variant
waefrebeorn
pushed a commit
to waefrebeorn/slermes
that referenced
this pull request
Jul 2, 2026
…rop-stale-vercel-group fix(nix): drop stale "vercel" group from #full variant
santhreal
pushed a commit
to santhreal/hermes-agent
that referenced
this pull request
Jul 13, 2026
…rop-stale-vercel-group fix(nix): drop stale "vercel" group from #full variant
Gravezzz
pushed a commit
to Gravezzz/hermes-agent
that referenced
this pull request
Jul 21, 2026
…rop-stale-vercel-group fix(nix): drop stale "vercel" group from #full variant
teknium1
added a commit
that referenced
this pull request
Jul 30, 2026
…ling-site wiring - Bump vercel SDK pin 0.5.7 -> 0.7.2 (pyproject, lazy_deps) and regenerate uv.lock - Disable the SDK's new default-on telemetry (VERCEL_TELEMETRY_DISABLED=1 set before import, user-overridable) per the no-opt-out-telemetry policy - Move _model_flow_ai_gateway into hermes_cli/model_setup_flows.py (god-file decomposition landed after the removal) - Widen post-removal backend sets that vercel_sandbox missed: terminal_tool container_backend + _CONTAINER_BACKENDS, file_tools fallback set, env_probe._REMOTE_BACKENDS, approval._should_skip_container_guards, prompt_builder probe container_config - Add terminal.vercel_runtime to config_defaults + TERMINAL_CONFIG_ENV_MAP - Re-add vercel dependency group to nix #full variant (reverts #33773 workaround) - Update restored tests to current contracts: upload-only credential sync-back (bcfc745), registry-derived provider env list, parametrized backend fixture, drop tests superseded on main (slack wizard move #41112, nous status format)
SSC-ENG
added a commit
to SSC-Engineering/hermes-agent
that referenced
this pull request
Jul 30, 2026
* fix(tests): eliminate flaky/broken tests — shadow sys.path inserts, unmocked network in compressor tests, stale-SDK feishu pin guard, quadratic redact regexes
- Remove tests/-shadowing sys.path.insert(dirname/'..') from 11 test files:
it prepended the tests/ dir itself to sys.path, so 'import agent' /
'import hermes_cli' resolved to the test packages and collection died
with ModuleNotFoundError depending on import order (2 files failed in
every full-suite run; 9 more were latent).
- Patch call_llm in 5 context-compressor tests that called compress()
unmocked: each burned ~50s attempting live LLM traffic through the
relay before falling back (572s file — the slowest in the suite, and
flaky under the 300s per-file timeout). File now runs in ~5s.
- agent/redact.py: fix two catastrophically-backtracking regexes hit by
the compressor's redaction pass on large payloads —
_STRICT_URL_USERINFO_RE anchors on the mandatory '//' (optional-scheme
prefix backtracked O(n^2): ~55s on a 320KB payload, now sub-ms;
output-equivalence fuzz-verified on 20k random strings), and the
_CFG_DOTTED_RE/_CFG_ANCHORED_RE subs gain an exact linear keyword
pre-gate so secret-free text skips the quadratic pattern entirely.
- tests/gateway/test_feishu.py: version-guard the extra_ua_tags SDK
signature check; the repo pins lark-oapi==1.6.8 but stale local
installs (1.5.3) fail the assertion — skip below the pin.
- tests/tools/test_managed_browserbase_and_modal.py: stub
agent.redact + agent.credential_persistence in the fake agent package
(empty __path__ blocks all real agent.* imports added since the fake
was written).
- tests/gateway/test_startup_restart_race.py: raise wait_for timeouts
2s -> 30s; 2s wall-clock on a loaded 40-worker box flaked in the
baseline run (passes instantly when the box is quiet).
* test: prune low-value tests suite-wide (wave 1) — 46,820 → 28,106 test functions
Systematic prune per AGENTS.md test policy, one pass over every major
test tree (gateway, hermes_cli, tools, agent, run_agent, plugins, cli,
cron, tui_gateway, honcho/openviking, root-level):
- DELETE: source-reading tests (read_text/getsource on prod files),
change-detector tests (exact catalog counts, model-name snapshots,
config version literals), mock-echo tests (assert a mock returns what
it was told), assertion-free/trivial tests, near-duplicate
parametrizations (boundaries + one representative kept), async/sync
twin duplicates, cosmetic within-file variations.
- KEEP (mandatory): security/redaction/approval guards, message-role
alternation invariants, prompt-caching/deterministic-call-id
invariants, issue-number regression tests (deduped), E2E tests.
- 6 test files deleted outright (script-style/no-assert or fully
redundant); conftest.py, fakes/, fixtures/ untouched.
- tests/acp/conftest.py added: autouse fixture stubs the live
models.dev/GitHub/Copilot/Anthropic inventory fetches that ACP server
tests performed on every session create — test_server.py 147s → 3.4s,
and the tests are now genuinely hermetic.
- Sleep-based slowness shrunk where safe (codex_ttfb_watchdog,
compression_concurrent_fork, etc.); no wall-clock assertion tightened.
Verification: full hermetic suite via scripts/run_tests.sh —
2439 files, 31,130 tests passed, 0 failed, 0 flaky retries, 315s wall
(baseline: 583s wall, 13,564s subprocess CPU).
* test: prune wave 2 + speed fixes — 28,106 → 19,757 test functions, suite wall 315s → 294s
Second, deeper pass over tools/gateway/hermes_cli plus first pass over
the trees wave 1 missed (acp, acp_adapter, skills, computer_use, docker,
dashboard, conformance, monitoring, secret_sources, hermes_state,
providers). Same rubric as wave 1 (AGENTS.md test policy); security,
alternation/caching invariants, issue-number regressions, and E2E kept.
Real test-quality fixes found and rooted out along the way:
- tests/tools/test_command_guards.py made real auxiliary-LLM HTTPS calls
(DEFAULT_CONFIG smart-approval leaked in) — pinned approval
mode=manual via autouse fixture: 17.4s → 0.4s.
- test_model_switch_custom_providers.py / test_user_providers_model_switch.py
silently probed live provider catalogs (~2s/test) — stubbed
cached_provider_model_ids/provider_model_ids/fetch_api_models.
- test_telegram_noise_filter.py: 15-platform copy-paste matrix over
shared gateway.run logic → 3 representative platforms (55s → 3.9s).
- test_gateway_shutdown.py: stop()'s 5s interrupt-deadline loop spun on
MagicMock agents — interrupt.side_effect now clears _running_agents
(22s → 1.0s).
- test_gateway_inactivity_timeout.py poll-harness timings shrunk 3-5x
(24s → 1.1s); test_mcp_stability.py backoff/SIGTERM-grace sleeps
patched (15.4s → 2.5s); test_async_delegation.py negative-drain wait
5s → 0.5s.
- test_telegram_init_deadline.py: loop-block margin restored to 1.0s
with rationale comment — the watchdog-dump assertion needs the loop
blocked well past deadline+grace under parallel load (flaked once in
the 40-worker verification run at a 0.2s margin).
Verification: full hermetic suite via scripts/run_tests.sh —
2,438 files, 21,718 tests passed, 0 failed, 293.9s wall.
Suite totals vs original baseline: 46,820 → 19,757 test functions
(−57.8%), wall 583.5s → 293.9s (−50%), subprocess CPU 13,564s → 11,623s.
* feat(desktop): platforms.changed broadcast retires the Messaging page's 6s status poll
The change watcher (#73673) missed one always-on-while-mounted timer: the
Messaging page polled /api/messaging/platforms every 6s for connection
status. The gateway already persists platform connect/disconnect/health to
gateway_state.json, so watch that file's mtime and broadcast
platforms.changed (floored to 5s — the gateway also rewrites the file for
in-flight-count bookkeeping), route it through live-sync like its
siblings, and refresh the page on the tick. Older backends keep the
legacy visible-tab poll verbatim.
Finishes the always-on poll sweep for #73618.
* fix(installer): never let a stale --commit pin roll an install backwards
hermes-setup.exe bakes its build-time commit into the binary
(BUILD_PIN_COMMIT) and passes it as -Commit on every install-mode run,
including the retry the desktop's "Update didn't finish" screen kicks
off. The repository stage checked that SHA out unconditionally, so an
installer built months earlier rewound a current managed checkout to its
build commit -- 9,160 commits in the reported case -- leaving ancient
source against a current venv. npm then failed on workspaces that did not
exist yet at that commit, and every later update ran against the wrong
tree.
Skip the pin when its target is already an ancestor of HEAD. Fresh clones
have no such ancestry so reproducible/CI pinning is unchanged, and
--force-commit / -ForceCommit still rolls back on purpose.
* fix(update): make the in-progress marker a real cross-process lock
Three surfaces start updates against one checkout: a terminal
"hermes update", the dashboard's Update button (which spawns that same
command detached), and the desktop's, which hands off to the Tauri
updater. Only the Tauri updater published the in-progress marker, and
only Electron read it -- to gate backend startup, not to stop a second
updater. So a dashboard-spawned update and an installer-driven git
checkout could mutate the same tree concurrently, rewriting source under
a live interpreter.
Claim the same marker from cmd_update rather than adding a second
mechanism: same path, same pid+started_at payload the Rust and Electron
readers already parse. A marker only counts as live when its pid is alive
and it is inside the shared age ceiling, so a crashed updater self-heals
instead of wedging every future update. Release only removes a marker we
still own, leaving a handoff partner's claim intact.
Refusing exits 2, matching the existing concurrent-instance contract the
Tauri updater already recognizes.
* fix(installer): refuse a second updater instead of clobbering the marker
UpdateMarkerGuard::acquire overwrote the in-progress marker
unconditionally, so a Tauri update launched while a dashboard-spawned
"hermes update" was mid-flight simply took the marker and ran a second
updater over the same checkout. That is the race behind the reported
Windows failure: install-mode bootstrap rewound the tree while the
dashboard's updater was still running npm install against it.
acquire now returns Result and refuses when a live foreign owner holds
the marker, and Drop no longer deletes a marker this process does not
own. Liveness matches the Python and Electron readers of the same file:
dead pid or past the shared age ceiling means stale and reclaimable, so
a crashed updater cannot wedge future updates.
Adds a cfg(unix) libc dependency for the signal-0 liveness probe; the
Windows path uses OpenProcess/GetExitCodeProcess.
* fix(installer): release the update marker on success, not just on drop
The updater keeps a Tauri/Cocoa event loop alive while it relaunches the
desktop, and that loop can outlive app.exit(0). Relying on Drop alone
left a *successful* update looking active -- a live pid holding a fresh
marker -- which blocked desktop startup and, now that the marker is also
the cross-process update lock, every subsequent updater until the age
ceiling expired.
Release explicitly once all install-tree mutations are done, before the
relaunch. complete() is idempotent so Drop still covers the failure and
panic paths. Arms a process-exit fallback so a wedged event loop cannot
leave a finished updater lingering as a live pid.
Co-authored-by: nateEc <nateEc@users.noreply.github.com>
* fix: approve listed pairing requests
* test: cover pairing CLI approval paths
* fix(pairing): reset the failed-approval counter on a successful approval
approve_code()'s success path never cleared _failures:{platform}. The
counter is incremented on every non-matching code, persisted in
_rate_limits.json, and only ever reset to 0 when it reaches
MAX_FAILED_ATTEMPTS (firing the lockout). So it counts failures over the
gateway's entire lifetime, not consecutive ones.
An owner who mistypes a pairing code on a handful of separate occasions
— each time immediately retyping it correctly and successfully pairing —
accumulates those isolated typos. A later single fresh typo then hits
MAX_FAILED_ATTEMPTS and locks the whole platform out for an hour, at
which point _is_locked_out gates approve_code and even the *correct*
code is rejected.
Reset the counter on a successful approval, matching standard
brute-force-guard semantics (the counter tracks consecutive failures).
This does not weaken protection: an attacker cannot produce a success
without a valid code, and 5 consecutive wrong attempts still lock out.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(pairing): keep GUI approvals off the code brute-force lockout
Follow-up hardening on the request-id grant path.
approve_request took the same lockout treatment as approve_code: gated by
it, and recording a miss toward it. But the two paths defend different
things. The lockout exists to stop guessing at the 8-char code space over a
messaging channel; a request id is only ever obtained by an admin already
authenticated to the store, so a miss means the row they clicked went stale.
Counting those let a handful of clicks on a stale list lock the operator out
of `hermes pairing approve` for an hour — the GUI DoSing the CLI.
Also drops the `code`/`code_hash_prefix` compat fields from list_pending.
The hash prefix is what admin surfaces mistook for an approvable code in the
first place, and re-exporting the request id under the old `code` key just
preserves the ambiguity; both consumers in the tree read `request_id` now.
The 16-hex sniffing that had been copy-pasted into the CLI and the endpoint
(where a chained conditional consulted it against the wrong field) moves to
one owner, PairingStore.looks_like_request_id.
The endpoint no longer reports a 429 on the request-id path, where lockout
can't apply — a stale id surfaced as a bogus "locked out" while the platform
sat locked for something else entirely.
* fix(update): use the no-kill pid probe, not os.kill(pid, 0)
The Windows-footgun linter caught a real bug in the new update lock. On
Windows os.kill(pid, 0) is not a no-op: CPython routes sig=0 to
GenerateConsoleCtrlEvent, which sends Ctrl+C to the target's entire
console process group (bpo-14484). The liveness probe would have killed
the very updater it was asking about -- and any sibling sharing that
console.
Delegate to gateway.status._pid_exists, the project's existing no-kill
probe, which uses psutil (OpenProcess/GetExitCodeProcess on Windows) and
also reports zombies as dead. Any pid we cannot evaluate still counts as
dead so a corrupt marker cannot wedge the lock.
* fix(desktop): stop an unowned publisher poisoning the thread clearance at :root
The thread's bottom clearance is composer + status-stack + 2rem, and both
inputs are measured by JS onto the owning [data-chat-surface]. The surface-var
helpers fell back to document.documentElement when they couldn't resolve one —
but :root is where every surface's DEFAULTS live, so a single stale write there
becomes a global floor under every thread's clearance until reload.
An unowned publisher has nowhere to publish to, so it now publishes nowhere.
* refactor(desktop): move the main-is-occupied check into the session door
newSessionOpensTab answers a question that isn't specific to the sidebar
"+" — is there a conversation on main that must not be discarded — and the
palette needs the same answer. Fold it into open-session as
mainChatOccupied so both callers share one definition.
* feat(desktop): find and reuse an open blank "New session" tab
An unused draft tab is the one a user would have typed into, so give the
tile store a way to name it (blankDraftTile) and hand it to another
session in place (reuseBlankDraftTile). A blank-but-busy tab has its
first turn in flight and an unbound tile is unknown rather than empty, so
neither is a candidate.
* fix(desktop): stop ⌘K and notification clicks stealing the main tab
Both surfaces passed the sidebar's in-place intent, which means "load it
into main when it isn't already on screen" — right for a row you clicked
in a list you were looking at, wrong for a chat opened from outside the
workspace. Neither had a surface of its own, so they took the one you
were using.
Add a stack intent for that case. It focuses the session when it's
already open, spends an unused draft tab when there is one, and only
falls back to main while main is itself a blank draft. Modifiers still
force a tab or window.
* feat(desktop): tab verbs follow the pane under the pointer
⌘1…⌘9, ⌃Tab, and the ⌘W / ⌘T family all resolved the last-interacted
zone, so switching tabs in a second pane meant clicking into it first.
They now resolve the HOVERED zone when the pointer is in one, falling
back to the focused zone otherwise — hover pane 1, ⌘2; hover pane 2, ⌘2,
and each lands in its own strip.
tabTargetGroupId() is the single resolver, keeping the number keys and
the tab verbs from disagreeing about which zone is "the" zone the way
they already couldn't. pointerover fires per boundary crossing rather
than per mouse move, and leaving the document clears the override so a
parked pointer never strands the keys on a stale zone.
* refactor(gateway): extract run_sync onto TurnRunner (completes the TurnContext seam; AST-identical body)
* refactor(web): extract sessions/mcp/skills/tools routes to APIRouter modules (wave 2; route-table equality verified)
- hermes_cli/web_routers/sessions.py: 14 routes across 3 routers
(list_router, search_router, manage_router) mounted at the three original
registration points so global route order is preserved exactly.
- hermes_cli/web_routers/mcp.py: 11 routes; OAuth flow registry
(_mcp_oauth_flows/lock/cap) stays in web_server, reached via new
web_deps.LateState live proxies so tests mutating web_server._mcp_oauth_flows
keep working.
- hermes_cli/web_routers/skills.py: 12 routes across hub_router + router
(two original registration points straddle the profiles router include).
- hermes_cli/web_routers/tools.py: 12 routes; toolset/terminal catalogs stay
in web_server (some are defined after the mount point), reached via LateState.
- web_deps.py: add LateState — operation-time proxy for web_server-owned
module state (getattr/item/iter/len/contains/context-manager/comparisons).
- Handler bodies byte-identical; legacy re-exports keep
web_server.<handler> importable for tests.
- Verified: ordered route table (method, path) identical to pre-refactor app
(291 routes); import smoke; ruff; windows-footguns clean.
- test_web_server_sessiondb_eventloop.py: structural AST scan now reads both
web_server.py and web_routers/sessions.py (handlers moved; helpers stayed).
* feat(desktop): approve pairing requests from the messaging page
Desktop had no pairing surface at all. Someone DMs the bot, gets a code,
and lands in pending — where only the dashboard or `hermes pairing approve`
could let them in. That gap is why the dashboard's broken approve button
went unnoticed for so long: desktop users never saw the flow.
This puts it where the decision already lives. The messaging page is
already master-detail by platform, already polls every 6s, and already owns
"who can talk to this bot" — the allowlist env vars in the same detail pane
are what an approval writes into. A pending block sits above the credential
fields (approving is the hot path); a count badge on the platform row is the
discovery mechanism, so you see "Telegram 2" whenever you open Messaging for
any reason. Neither renders when nobody is waiting: approvals are rare, and
a permanent empty state would be chrome on a page that is otherwise about
credentials.
Approval sends the row's request_id and never a code — the code is the
requester's proof that the channel is theirs and is never returned by the
API. Rows paint optimistically from a snapshot and roll back visibly on
failure, and a 429 gets its own message since the code path's lockout is a
condition the operator can only wait out.
Pairing rides the existing platform refresh via allSettled, so a backend
without the endpoint yields no rows instead of blanking the page.
* style(desktop): sort the confirm-dialog import (eslint perfectionist)
* fix(gateway): scope pairing platform discovery to the profile dir
The per-profile pairing isolation added self._dir and scoped every
per-file path helper (_pending_path, _approved_path) to it, but
_all_platforms still enumerated the module-global PAIRING_DIR. For a
profile-scoped PairingStore, list_approved/list_pending/clear_pending
therefore operated on the GLOBAL platform set while loading each
platform's file from the PROFILE dir — so list_approved() returned []
for a user that is_approved() confirmed as approved, a silent divergence
between the authz surface and the list/inspect/clear surface.
Route discovery through self._dir. Byte-identical for the global store
(self._dir == PAIRING_DIR when no profile is set); only the buggy
profile-scoped case changes. self._dir is guaranteed to exist (__init__
mkdirs it).
* test(gateway): patch hermes_constants.get_hermes_home so profile store scopes to the mocked home
* fix(gateway): align multiplex pairing stores
Co-authored-by: x7peeps <x7peeps@users.noreply.github.com>
* fix(pairing): scope the approve/revoke endpoints to a profile
The gateway keeps one PairingStore per served profile, but every
`/api/pairing` endpoint built the global one. An operator managing a named
profile saw the wrong pending list, and approving wrote a grant into a
whitelist their running gateway never consults — the user stays locked out
while the UI shows them as approved.
`_pairing_store(profile)` now resolves per profile and validates the name
(400/404 on an unknown one). No `_profile_scope` needed: PairingStore
resolves the profile's home itself, so nothing process-global is swapped
across an await.
Both GUIs had to change to match. The listing rides the query param — for
the dashboard that meant deleting `pairing` from the "machine-global, must
NOT be rewritten" exclusion list, a comment this change makes false. The
mutating endpoints read the profile off the BODY, which no query-param
rewrite reaches, so approve/revoke send it explicitly on both surfaces.
* refactor: config auto-migration support floor at v12 + deprecated shim retirement
* fix: floor gate only refuses configs with an EXPLICIT below-floor _config_version
Profile clones and hand-written minimal configs carry no _config_version
key; check_config_version() coerces that to 0, which wrongly tripped the
v12 floor and left clones unstamped (caught by CI:
test_clone_config_copies_files / test_clone_from_named_profile).
Version-less configs now take the normal ladder + fresh stamp — the
historical behavior; only genuinely ancient explicit-version configs
are refused.
* fix(desktop): treat landed memory writes as success, not warnings
Trust explicit success over a stale isError envelope so real saves stop
painting amber. Over-budget refusals keep a soft warning with "Memory
write noted" instead of crowing "Saved", and entry_count labels as
entries.
* style(desktop): gold→purple chrome on landed memory saves
Paint successful memory tool rows with a gradient title, tinted brain
glyph, and purple meta so a save reads prized rather than like a warning.
* feat(pairing): give pairing its own change signal
The poll this page's pairing block rode was retired hours earlier by
f8e07a332, which moved Messaging onto platforms.changed. That signal is the
mtime of gateway_state.json — where the gateway persists connect/disconnect
health — and a new pairing request moves none of it. So on an event-capable
backend a pending row stayed invisible until something unrelated
reconnected, and the count badge with it.
Adds pairing.changed to the change watcher, signed off the pending/approved
ledgers across the global store and every profile's own. _rate_limits.json
is deliberately excluded: it moves on every unauthorized DM, including ones
that produce no new row, so signalling on it would refetch for nothing.
The page now refreshes platforms and pairing on their own signals rather
than one combined call, and the legacy visible-tab poll (older backends)
covers both.
* fix(desktop): stop composer chips demoting to plaintext on every re-render
Two halves of one bug class: the composer treated 'rebuild the editor
from serialized text' as routine, and serialized text couldn't express a
slash pill.
In-place commits: Chromium fragments text nodes around
contenteditable=false chips, so the old commit path — whole token in ONE
text node with the caret at its end — failed almost every keyboard pick
and fell into the rebuild fallback. rangeBeforeCaret now walks the token
backwards across sibling text nodes and refuses only at real boundaries
(chip, <br>, block), making in-place replacement the default for picks,
folder descends, Backspace path-ascends, and action items in both the
main composer and the user-edit composer.
Slash-pill hydration: renderComposerContents re-chipped @kind:value refs
but never /command, so any surviving rebuild (draft restore, undo, the
fallback above) demoted a committed command pill. A leading no-arg
/command now hydrates back to its pill; arg-taking commands stay text
since their tail may be uncommitted prose.
Also: popover picks bank an undo point in the main composer, and Tab is
swallowed while completions are in flight instead of moving focus out of
the composer.
* fix(managed_uv): repair vulnerable SQLite runtime in .venv installs too
repair_vulnerable_runtime() hardcoded <checkout>/venv as the live venv,
so uv-default/dev checkouts installed into .venv got 'not-applicable' on
every hermes update — no repair path ever fired, leaving state.db-class
DBs on journal_mode=DELETE forever (measured 26 ms + ~5.5 fsyncs per
append vs ~0.01 ms under WAL, ~2,600x) while the WAL fallback warning
falsely promised hermes update would repair the runtime.
- _default_live_venv(): target venv/ when it has an interpreter (managed
layout precedence), fall back to .venv/, keep not-applicable when
neither exists. Explicit venv_dir arg unchanged; all staging/smoke/
cutover/rollback machinery untouched.
- Rebuilt against the pruned test suite (main's test-prune waves 1+2
rewrote test_managed_uv.py, so this reapplies cleanly): 3 new
TestDefaultLiveVenv tests + repair neutralized in the 6 unit tests
whose subject is uv install/self-update mechanics — with .venv now
probed for real, CI's own vulnerable .venv made the unmocked repair
hook fire inside those tests and re-invoke _install_uv.
33/33 tests green on the pruned suite.
* fix(desktop): keep chips atomic to composer trigger detection
textBeforeCaret serialized chip labels into the string the trigger
regexes scan, so a leading /work pill made the anchored command regex
swallow the rest of the line as its argument — silencing the @ popover
for the whole message. Chips now contribute an object-replacement
placeholder and <br> a newline, so committed pills can't poison
detection.
* test(desktop): cover the composer chip plaintext-demotion bug class
Backspace path-ascend keeping the leading command pill, folder picks
alongside a command pill, commits spanning Chromium-split text nodes,
slash-pill hydration boundaries (committed vs half-typed vs arg-taking),
and replaceBeforeCaret refusing across chip boundaries.
* fix(telegram): batch near-limit command chunks so split /queue pastes don't orphan their continuation
Telegram clients split messages above 4096 chars into multiple updates. A
long '/queue <prompt>' paste arrives as a COMMAND chunk near the limit plus
plain TEXT continuation chunk(s). _handle_command dispatched the command
chunk immediately, so the continuation landed as a separate plain message
that interrupted the running agent instead of being queued.
Near-limit (>= _SPLIT_THRESHOLD) command chunks now route through the same
text-batching pipeline used for split plain-text messages, merging the
continuation before dispatch. Short commands (/stop, /approve, ...) keep the
immediate path and are never delayed.
* fix(state): time-based write-lock patience so busy sibling processes can't destroy turns
A shared state.db is legitimately held for multi-second stretches by
sibling Hermes processes: VACUUM after auto-prune, the TRUNCATE WAL
checkpoint at close on a large WAL, offline recovery, or an older
still-running process whose FTS maintenance predates the bounded-merge
protocol (every `hermes update` leaves mixed-version processes sharing
the DB until the old ones exit).
The old retry budget was attempt-counted: 15 attempts x 20-150ms jitter
gives up after ~1.3s of waiting. Any hold longer than that surfaced as:
- append_message failing -> the conversation loop aborts the turn as
session_persistence_failed ('No reply: the turn was stopped because
session storage could not be written') on a perfectly healthy store;
- SessionDB() open failing -> the CLI disables persistence for the
entire run ('Failed to initialize SessionDB ... database is locked').
Both observed in production logs on 2026-07-29 (10.8 GB state.db, 9
concurrent hermes processes, three of them pre-dating the bounded-merge
fix pull).
Changes:
- _execute_write patience is now TIME-based with two budgets: routine
writes wait up to 20s; transcript-critical writes (append_message,
session-row creation — the ones whose failure aborts a user turn)
wait up to 60s. Jitter stays 20-150ms for the first 2s, then backs
off to 250ms-1s so a long hold isn't hammered with BEGIN IMMEDIATE.
- Exhausted patience raises an error that names the actual cause
(another process held the write lock; the database is healthy)
instead of a bare 'database is locked' that reads like disk damage —
and the turn-abort explainer inherits that clarity.
- SessionDB open now applies the same jittered patience to the
locked/busy class around connect+schema-init, instead of failing the
whole open (and disabling persistence for the run) on the first 1s
timeout. Non-lock errors, including the malformed-schema repair
class, propagate immediately as before.
Fixes #74478
* fix(gateway): relay TTS attachments + semantic auto-thread rename on the title turn (#74482)
Two relay-lane bugs from live Discord staging testing (2026-07-29):
1. TTS audio never attached over relay (any platform, any lane).
_history_media_paths_for_session excluded only the trailing assistant
entry from the persisted transcript when building the delivered-media
dedup set. The agent persists rows as it produces them, so THIS turn's
text_to_speech tool result (media_tag JSON) was already in the
transcript at delivery time — the fresh TTS path deduped against
itself and extract_media's attachment was silently stripped
(response_delivery_dropped for a MEDIA-tag-only reply; fly logs show
the exact signature). Fix: exclude everything from the last USER
message onward (the current turn); prior-turn dedup unchanged.
Affects every platform adapter (native + relay) on the non-streaming
delivery path — the streaming path passes explicit history and was
unaffected.
2. Connector-auto-created threads never got the LLM session-title
rename. The title fires on the FIRST exchange, whose source is the
PARENT channel event — the thread didn't exist at ingest, so the
Phase 4 auto-thread markers can't be present and
_is_discord_auto_thread_lane never matches on the relay title turn
(initial titles worked; semantic renames never happened; staging
telemetry shows zero thread_rename ops ever sent). Fix: consume the
connector's new send-result feedback (paired gateway-gateway PR —
contract §SendResult thread_id/auto_thread_name, additive):
RelayAdapter.send() caches (thread_id, initial_name) per chat
(bounded 256), run.py's title-callback registration + rename lane
read it back and pass initial_name as only_if_current_name so the
human-rename-wins guard holds on the relay lane too. Native marker
path unchanged; connectors that don't stamp the fields degrade to
exactly the old behavior.
Tests: 3 new (send-result feedback capture, absence, bound) in
test_relay_threads.py; 3 new in test_history_media_current_turn.py
(current-turn TTS not deduped, prior-turn still deduped, no-user-row
fallback). Relay suite 144 passed.
* fix(state): configurable journal_mode + centralize all DB openers
Add HERMES_JOURNAL_MODE env / database.journal_mode config for
virtiofs/NFS/SMB where WAL is not crash-safe. Route 5 bypass openers
through apply_wal_with_fallback so a single setting covers every .db
(#68545).
* fix(state): restore async_delegation.py symbols, keep only journal_mode routing
The previous commit accidentally reverted async_delegation.py to a
pre-origin_session_id snapshot, dropping _MAX_DELIVERY_ATTEMPTS,
_current_origin_session_id, _transaction(), the origin_session_id
column, and drop_completion_delivery() — causing ImportError in
delegate_tool.py and kanban_tools.py.
This restores the upstream main version of async_delegation.py and
re-applies only the journal_mode routing change (db_label update to
'async_delegation.db').
Addresses reviewer feedback on #68912.
Signed-off-by: Jasmine Naderi <jasmine@smfworks.com>
* fix(state): make journal mode canonical and behaviorally verified
Use database.journal_mode as the sole non-secret operator setting, preserve the vulnerable-SQLite safety gate and existing WAL databases, validate explicit DELETE results, document the active config path, and cover real SQLite openers with behavioral tests.
* fix(gateway): add disk I/O error to WAL-fallback markers for ZFS (#55305)
* fix(state): detect silent WAL→DELETE fallback on macOS NFS / SMB (no false "wal")
apply_wal_with_fallback() only detected WAL-incompatible filesystems via a
RAISED OperationalError matched against _WAL_INCOMPAT_MARKERS. But macOS NFS,
SMB/CIFS, and overlay filesystems (e.g. AgentFS's NFS-backed mount) refuse the
WAL switch WITHOUT raising: `PRAGMA journal_mode=WAL` returns the still-effective
mode ('delete') and no exception. The code then ran `return "wal"`
unconditionally, so it:
1. returned a false "wal" while the DB was actually in DELETE mode, and
2. never called _log_wal_fallback_once, so the operator got ZERO signal that
concurrency had silently degraded (reader-blocks-writer).
state.db and kanban.db share this path, so a session/kanban board DB on a
network or overlay filesystem ran in DELETE with no diagnostic.
Fix: read the row `PRAGMA journal_mode=WAL` returns and verify it is actually
'wal' instead of assuming success; on a silent no-op, emit the existing
fallback WARNING and return the true mode. The raise-based path is unchanged.
Reproduced on a real AgentFS NFS overlay (PRAGMA journal_mode=WAL returned
('delete',) with no OperationalError). Adds a regression test for the
silent-no-op shape; the 16 existing WAL-fallback tests are unchanged.
* fix(state): escalate silent WAL→DELETE fallback to ERROR + opt-in require_wal
The WAL→DELETE fallback on WAL-incompatible filesystems (NFS / SMB / FUSE /
the AgentFS NFS overlay) was logged at WARNING, treating a real loss of
concurrency — under the kanban dispatcher + workers a write blocks readers,
surfacing as SQLITE_BUSY — as if it were cosmetic. Escalate the deduplicated
fallback log to ERROR so the degradation is observable, not silent.
Add an opt-in require_wal=True to apply_wal_with_fallback that raises a typed
WalUnsupportedError (subclass of sqlite3.OperationalError, so existing DB-init
handlers still catch it) instead of degrading to DELETE, for callers that
mandate WAL concurrency. All four current callers keep the default
require_wal=False so NFS-homed installs keep working unchanged.
Tests: 4 new require_wal cases; WARNING→ERROR assertion updates in both
test_hermes_state_wal_fallback.py and test_kanban_db.py.
* test(state): prove silent WAL fallback in callers
* fix(state): retry transient disk I/O errors before WAL fallback
Salvages #55322 (ZFS 'disk i/o error' marker) without regressing the
Bug D transient-EIO protection from 5c49cd0ed0. 'disk i/o error' is
ambiguous: deterministic on ZFS/APFS-CoW SHM corruption (#55305,
#71498) but often a one-shot transient (page-cache pressure, lock
contention). A blind marker match re-introduced the mixed-journal-mode
corruption pattern; a blind re-raise wedged state.db on ZFS.
Disambiguate: retry the WAL pragma twice with a short backoff. A
transient EIO clears and WAL proceeds; a deterministic failure keeps
raising and falls through to the guarded DELETE fallback (still
refusing to downgrade a DB whose on-disk header reports WAL).
Tests: transient-EIO recovery, persistent-EIO fallback, and the
never-downgrade-WAL-on-disk guard.
* fix(config): respect database.journal_mode from config.yaml
Config-driven `journal_mode`, `wal_autocheckpoint`, and `journal_size_limit`
are now honored in `SessionDB` init. Users running SQLite on NFS/SMB or
with custom tuning had no supported config surface; values were hardcoded
at connection time.
What
- New `apply_database_pragmas()` in `hermes_state.py`
- Reads nested `database:` keys from `config.yaml` via existing `cfg_get`/`load_config`
- Called after `apply_wal_with_fallback()` in `SessionDB._connect_and_init()`
Fix
- Adds optional PRAGMA switches for journal_mode, wal_autocheckpoint, journal_size_limit
- On Darwin; Windows keeps DELETE unless config explicitly requests WAL
Runtime Proof
$ /opt/homebrew/bin/pytest tests/test_hermes_state.py::TestApplyDatabasePragmas -q
3 passed in 0.72s
Regression Checks
- Full `tests/test_hermes_state.py`: 306 passed in 11.32s
* fix(state): reconcile salvaged WAL fixes with current main
- apply_database_pragmas: journal_mode ownership stays with
apply_wal_with_fallback/resolve_journal_mode (single guarded owner);
the helper now only applies wal_autocheckpoint / journal_size_limit,
via load_config_readonly (hot-path safe).
- Silent-refusal WAL success path re-applies the macOS
checkpoint_fullfsync barrier and synchronous=FULL enforcement.
- Test doubles updated for connect_tracked's factory kwarg and the
WAL-reset vulnerability gate (fixed-SQLite assumption made explicit).
* chore: map salvaged contributor emails for attribution
* fix(config): register WAL sizing pragmas in DEFAULT_CONFIG
database.wal_autocheckpoint / database.journal_size_limit are now real
schema keys (default None = SQLite defaults) so the dashboard config
schema doesn't produce a single-field 'database' category, and the two
pragmas apply_database_pragmas reads are discoverable/documented.
* fix(cli): remove venv.stale.runtime marker after successful managed runtime repair
* fix(cli): sweep aged venv.stale.runtime-* backups on hermes update
Follow-up to the salvaged success-path removal: installs that already
repaired (or predate the cleanup) still carry leaked ~1 GB parked venvs.
When the runtime probes safe, reclaim aged (>1h) stale markers next to
the live venv — age-gated to avoid racing an in-flight sibling repair,
boundary-checked via _remove_tree so symlinked names can't escape the
checkout. Also drop the now-stale 'before removing the parked venv'
user guidance in update_cmd.
Tests: success-path removal, safe-path sweep (aged removed, fresh kept).
* chore: map webtecnica contributor email for attribution
* fix(gateway): preserve explicit send-image requests from session-wide MEDIA dedup
Closes #73771
The session-wide MEDIA dedup in (base.py)
filtered ALL media paths against prior-turn history, including explicit
MEDIA: tags the model deliberately included in its response text. When a
user asked the agent to resend an image, the dedup silently swallowed it
because that path already existed in the session transcript.
The dedup is already handled correctly by the auto-append path in
(run.py), which scopes its scan to the current turn and
filters against via .
The base.py filter was redundant for auto-appended tags and harmful for
explicit ones.
Fix: remove the dedup filter on in base.py while preserving
the variable for the dedup (bare file
paths, which lack run.py protection).
* fix(gateway): widen explicit-MEDIA resend fix to the streaming path + log bare-path suppression
Companion to the cherry-picked #74158 fix (non-streaming path):
- gateway/run.py: remove the identical history-dedup filter from
_deliver_media_from_response — the post-stream rescan is explicit-only
by design (#20834), so every MEDIA tag it finds is a deliberate
attachment request; drop the now-unused history_media_paths parameter
and its call-site plumbing.
- gateway/platforms/base.py: log suppressed bare local file paths on the
surviving local_files history dedup (#73771 observability ask).
- tests: focused regression file covering explicit resend delivery on
both lanes, current-turn tool-echo poisoning, surviving bare-path
dedup + its log line, and the upstream auto-append dedup invariant.
Fixes #73771
* test(tests): assert SessionDB timeout without wall-clock
Replace elapsed-time checks with captured Future.result(timeout=...) values so the hang regression stays deterministic under parallel CI load.
* fix(tests): restore original module identities after vision-routing reloads (#61597)
_fresh_modules() in test_vision_routing_31179.py deleted agent.image_routing
(+siblings) from sys.modules and never restored the ORIGINAL modules — the
reloaded copies leaked. Any later same-process test holding function refs to
the original module (test_image_routing.py) then patched the reloaded copy
via mock.patch string targets, making patches invisible: order-dependent
failures (repro: 31179 file first → 2 failures; reversed → green).
Autouse fixture now snapshots the affected sys.modules entries and restores
them on teardown. Both orders + solo runs verified green.
Masked by the per-file-isolated canonical runner; bites anyone running
pytest tests/agent/ directly.
* test(approval): event-based waits for blocking-approval E2E polls (PR #63522 port)
Manual port of @jethac's #63522 onto the pruned tree: 3 of the original
6 fixed-budget poll sites survive (3-concurrent-agents wait, session-A/B
routing wait, two-session queue wait). Replaced each 2.5-5s hard-ceiling
poll loop with the PR's _wait_until(predicate) helper — generous 30s
ceiling reached only on genuine failure, instant return on the green
path, assert with message instead of silent fall-through.
Dropped: the 3 hunks targeting pruned code, and the unrelated
scripts/release.py mailmap hunk (AUTHOR_MAP is frozen; contributor
mapping handled via contributors/emails/).
* fix(test): hermetic env-detection tests — pin container/supervisor/HOME probes (#422)
The restart-routing, systemd-support, and subprocess-HOME tests asserted
branch behavior but left part of the real probe surface unmocked, so they
fail when the suite itself runs inside a container (self-hosted CI) or a
launchd-descended shell:
- /restart routing tests: the handler also consults the real /.dockerenv —
extract the inline probe to gateway.restart.is_container_restart_context()
(patchable seam, no behavior change) and pin it False; scrub ALL four
supervisor env markers (ambient XPC_SERVICE_NAME on macOS flipped one).
- supports_systemd_services tests: pin shutil.which('systemctl') and
is_container() so the test asserts the branch, not the host.
- copilot ACP real-HOME test: pin is_container() (auto mode prefers profile
home in containers) and scrub ambient HERMES_REAL_HOME/TERMINAL_HOME_MODE.
97 tests green on macOS dev box AND inside a docker CI runner container.
Co-authored-by: Kyzcreig <9063726+Kyzcreig@users.noreply.github.com>
* Refresh on upstream/main: resolve conflicts (no behavior change)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(tests): forward Windows location vars through the hermetic runner; patch Path.home() in hindsight _clean_env
Combines the Windows-hermeticity cluster (#67512 by @webtecnica, earliest;
#71112 by @Sanjays2402; #67196 by @anatolijlaptev1991-ctrl) into one fix:
- scripts/run_tests.sh: env -i forwarded only HOME, but native Windows
CPython resolves Path.home() from USERPROFILE (or HOMEDRIVE+HOMEPATH),
stdlib paths from LOCALAPPDATA/APPDATA, ssl/sockets need SYSTEMROOT,
tempfile needs TEMP/TMP — the strip broke collection tree-wide on
native Windows (issues #67385, #70813). Location vars (never
credentials) are now forwarded, each only when actually set, so
POSIX runs are byte-for-byte unchanged (probe-verified both ways).
PYTHONUTF8=1 added for legacy-codepage consoles printing the
runner's glyphs.
- tests/plugins/memory/test_hindsight_provider.py: _clean_env patched
HOME only; on Windows Path.home() ignores HOME. Now patches
Path.home directly into tmp_path (from #67196).
Not ported: #71112's guard test — it regex-reads run_tests.sh source,
which the test policy bans (never read source code in tests).
Fixes #67385. Fixes #70813.
* fix(test): patch _launch_configured_cwd in completion tests for hermeticity (#70041)
tui_gateway/server.py freezes _hermes_home at import time, so
_launch_configured_cwd() reads the developer's real config.yaml even
under the per-test HERMES_HOME redirect. Any absolute terminal.cwd in
the real config made _completion_cwd() ignore monkeypatch.chdir and
broke the completion tests on a pristine checkout.
Patch _launch_configured_cwd to None in the autouse _reset_fuzzy_cache
fixture and add a regression test pinning that _completion_cwd resolves
via os.getcwd() under tests.
Salvaged from PR #70148 by @smfworks (rebased onto the pruned suite).
* fix(session): resolve default state DB path at call time
DEFAULT_DB_PATH in hermes_state.py is computed at import time, freezing
the developer's real ~/.hermes even when a test fixture (or runtime
profile switch) later redirects HERMES_HOME. Any default SessionDB() —
e.g. gateway SessionStore — then opened the real state.db.
Add _default_db_path(): resolves get_hermes_home() fresh at call time,
while a deliberately re-pointed DEFAULT_DB_PATH (the established
monkeypatch escape hatch) still wins via an import-time snapshot
comparison, preserving existing test behavior. SessionDB.__init__ and
session_search's requirement check now use the resolver; explicit
db_path arguments are untouched.
Reimplemented from PR #11875 by @JorkeyLiu (original diff predates the
hermes_state rewrite); regression test ported and modernized.
* test(tui): scope close_race orphan-cleanup assert to own session key
test_session_create_close_race_does_not_orphan_worker asserted the
process-global len(unregistered_keys) >= 1: a leaked _build thread from
another session.create test in the same shard can append a foreign key
and falsely satisfy the assert. Scope both the wait loop and the assert
to this test's own stored_session_id, matching the own-key scoping the
no-race companion test already uses for the same flake class.
Salvaged from PR #46189 by @AIalliAI (rebased onto the pruned suite).
* fix(test): fail-closed kanban write guard prevents real HERMES_HOME pollution (#69283)
Autouse conftest fixture patches kanban_db.connect to refuse writes whose
resolved DB path lands under the REAL kanban root (captured at conftest
import time, before fixtures rewire the environment). Deny-list, not
allow-list, so hermetic tests moving HERMES_HOME to sibling tempdirs are
unaffected. Lazily attaches only when hermes_cli.kanban_db is already in
sys.modules.
Salvaged from PR #69385 by @smfworks; rebased by hand onto the pruned
conftest and adapted to guard on the resolved DB path (explicit db_path
or kanban_db_path()) rather than kanban_home() alone.
Co-authored-by: Jasmine Naderi <jasmine@smfworks.com>
* test(cli): remove importlib.reload seam from web_server session-token tests (#38034)
Extract _resolve_session_token() in hermes_cli/web_server.py so tests can
exercise token resolution directly instead of importlib.reload(ws), which
re-executed the whole module mid-suite (fresh FastAPI app + token) and
split module identity between test and app state.
Salvaged from PR #39038 by @rodboev (maintainer-endorsed direction);
rebased onto the rewritten test_web_server.py — dropped the PR's hunks for
test_falls_back_to_random_token's old body (test deleted in the prune,
re-added here in the PR's new form).
Co-authored-by: Rod Boev <rod.boev@gmail.com>
* test: guard browser and Keychain side effects suite-wide (#35404)
Re-port of PR #35464 onto the rewritten hermetic conftest:
- autouse _neutralize_webbrowser fixture records open/open_new/open_new_tab
and webbrowser.get() instead of launching a real browser
- autouse _neutralize_macos_keychain_creds defaults the Anthropic Keychain
reader to None, with an opt-in allow_macos_keychain marker
- regression tests in tests/test_hermetic_side_effect_guards.py
- tests/agent/test_anthropic_keychain.py opts in via pytestmark
Salvaged-from: #35464
Co-authored-by: y0shualee <yuxiangl490@gmail.com>
* fix(honcho): network-hermetic unit tests + lazy async writer start
Re-port of PR #67576:
- plugins/memory/honcho/session.py: start the async writer thread lazily on
first enqueue via _ensure_async_writer (idempotent, lock-guarded) instead
of eagerly in __init__; shutdown tolerates a never-started thread
- tests/honcho_plugin/conftest.py: package-wide socket guard so no honcho
unit test can reach a live server
- tests/honcho_plugin/test_network_isolation.py: regression tests
- test_async_memory.py / test_oauth_flow.py: rebased hunks onto the pruned
suite (pruned tests not resurrected)
Salvaged-from: #67576
Co-authored-by: eapwrk <eapwrk@gmail.com>
* test: opt install-ladder tests back into lazy installs under the hermetic gate
The HERMES_DISABLE_LAZY_INSTALLS=1 conftest gate (from #43782) correctly
blocks real mid-run pip installs suite-wide, but
TestInstallDependenciesRunner exercises the install ladder itself against
a fully mocked subprocess.run — it needs the gate open. Same
both-directions override pattern tests/tools/test_lazy_deps.py already
uses. Sibling sweep of all install_specs/_pip_install/ensurepip test
files: 274 tests green.
* chore: contributor mapping for eapwrk (honcho salvage, #67576)
* fix(desktop): stop cold-start resume homing focus to the workspace tab (⌘R tab persistence)
The layout tree persists the active tab, but every reload landed on main
anyway: boot's route resume sets $selectedStoredSessionId, and the
selection listener in store/session-states.ts treats every selection
change as a navigation — noteActiveTreeGroup(null) +
revealTreePane('workspace') — fronting the workspace tab over the
persisted active tile and then persisting that clobber.
A cold-start restore is a re-attachment, not a navigation, so
use-route-resume now arms a one-shot (markSelectionRestore) before
dispatching the window's FIRST resume; the selection listener consumes
it and skips homing exactly once. Every later resume — sidebar click,
route change, reconnect — homes as before, and starting on /new
consumes boot status too so a subsequent session open still homes.
* fix(gateway): exclude current-turn media from dedup
* test(gateway): cover current-turn TTS media dedup
* fix(gateway): collect quoted/spaced/home-relative MEDIA paths into the history dedup set
Salvaged from PR #73982 by Alexander Russell (@AlexxRussell) — the
collector half only. _collect_history_media_paths used only
_TOOL_MEDIA_RE, which misses quoted and spaced paths that the delivery
pipeline's extract_media grammar accepts; run text content through the
same extractor so the surviving dedup consumers (auto-append lane and
bare-path filter) see every path that could actually have been
delivered.
The PR's other halves (post-stream dedup snapshot plumbing, canonical
path comparison in _deliver_media_from_response, queued-followup
snapshot union) are moot after #74495 removed the post-stream history
filter entirely.
* chore: contributor mappings for FraserHum + AlexxRussell (#72542/#73982 salvage)
* feat(desktop): highlight matched letters in searchable pickers
Typing in the model picker dialog, edit-models dialog, or command
palette now marks WHY each row matched: every occurrence of the query
(per-term for the palette's AND matcher) renders as an accent-colored
semantic <mark>. cmdk's scorer exposes no match ranges, so the shared
HighlightMatches primitive mirrors each surface's own filter semantics
instead: literal substring for the pickers, split-on-whitespace terms
with merged overlapping ranges for the palette.
* fix(desktop): make the composer model dropdown's search commit honestly
Two fixes to the pill dropdown's filter, following the consensus across
VS Code, Zed, Open WebUI, and Cherry Studio:
- The pinned current model no longer rides along on a query it doesn't
match. It sat above the real matches looking like the top result, so
typing 'grok' and committing landed you back on the current model.
- Enter in the search field commits the first visible match (VS Code's
'so Enter works without pressing DownArrow first'). Radix highlights
nothing until an arrow key, so Enter used to dead-end; now
open → type → Enter is the whole switch.
Matched letters also render through HighlightMatches like the other
searchable pickers.
* feat(desktop): ⌘⇧M opens the model menu on the pane under the pointer
composer.modelPicker shipped unbound and opened the full-screen picker
dialog. It now defaults to ⌘⇧M — the chord LibreChat, Open WebUI, and
Cherry Studio independently converged on — and toggles the composer
pill's live dropdown instead, search field ready: ⌘⇧M → 'gr' → Enter
switches model in two keypresses.
Routing follows the tab-verb convention (#74447): the request lands on
the chat surface in the hovered zone first, then the active composer,
skipping hidden keep-alive tabs. With no chat surface on screen
(settings, profiles) the keybind falls back to the full dialog; with
the gateway closed the pill opens the dialog like a click would.
* fmt(js): `npm run fix` on merge (#74544)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
* feat(mcp): add Comfy Cloud to the MCP catalog (remote HTTP + native OAuth 2.1)
New catalog entry for Comfy Cloud's hosted remote MCP server at
https://cloud.comfy.org/mcp — Streamable HTTP with native MCP OAuth 2.1
(Dynamic Client Registration + PKCE), the same shape as the linear entry.
Nothing to install locally; Hermes's MCP client handles discovery and the
browser flow on first connect.
The server exposes ~30 tools for AI generation on Comfy Cloud: image /
video / audio / 3D via ComfyUI workflows (submit_workflow), curated
templates (run_template), and partner models like Flux, Kling, and Veo
(partner_generate), plus job lifecycle and discovery tools.
tools.default_enabled is left unset so the install-time checklist starts
all-on, mirroring the linear entry.
Also adds the per-entry data-files target in pyproject.toml per the
one-target-per-entry pattern documented there.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(mcp): curate comfy-cloud default tool set + drop legacy packaging line
- tools.default_enabled: 20-tool curated subset (discovery, generation,
job lifecycle, billing). The server exposes ~37 tools; all-enabled adds
~16-22k tokens of schema to every API call — larger than the entire
Hermes core toolset (~12.7k). Curated default lands at ~9-12k. Batch,
saved/shared workflow, and App Mode tools remain opt-in via
'hermes mcp configure comfy-cloud'.
- report_session_summary excluded from defaults per telemetry policy
(no outbound telemetry without explicit user opt-in).
- description trimmed to catalog guideline length.
- revert pyproject data-files line: the per-entry packaging enforcement
was removed (no-pip policy); blender/unreal-engine entries have no
data-files lines either.
* chore: contributor mapping for mattmiller@comfy.org (@mattmillerai)
* Revert "remove Vercel AI Gateway and Vercel Sandbox (#33067)"
This reverts commit febc4cfec0a79b175a430304765473c97e10622f.
* modernize re-added Vercel integrations: SDK 0.7.2, telemetry off, sibling-site wiring
- Bump vercel SDK pin 0.5.7 -> 0.7.2 (pyproject, lazy_deps) and regenerate uv.lock
- Disable the SDK's new default-on telemetry (VERCEL_TELEMETRY_DISABLED=1
set before import, user-overridable) per the no-opt-out-telemetry policy
- Move _model_flow_ai_gateway into hermes_cli/model_setup_flows.py (god-file
decomposition landed after the removal)
- Widen post-removal backend sets that vercel_sandbox missed: terminal_tool
container_backend + _CONTAINER_BACKENDS, file_tools fallback set,
env_probe._REMOTE_BACKENDS, approval._should_skip_container_guards,
prompt_builder probe container_config
- Add terminal.vercel_runtime to config_defaults + TERMINAL_CONFIG_ENV_MAP
- Re-add vercel dependency group to nix #full variant (reverts #33773 workaround)
- Update restored tests to current contracts: upload-only credential sync-back
(bcfc7458fa), registry-derived provider env list, parametrized backend fixture,
drop tests superseded on main (slack wizard move #41112, nous status format)
* test: fix restored-test regressions vs current main
- test_terminal_requirements.py: restore missing 'import pytest' (revert
resurrected a parametrized test into a file whose pytest import was
pruned on main)
- test_container_cwd_sanitize.py: _CONTAINER_BACKENDS pin now includes
vercel_sandbox
* test(cli): fix order-dependent test_resume_quiet_stderr flake at the source
test_session_not_found_goes_to_stdout_in_full_mode passes in isolation but
fails in a full tests/cli run. Two independent leaks from the same neighbor
test conspire:
1. test_cli_init.py's _make_cli() reloads cli.py while prompt_toolkit is
stubbed with MagicMocks and never reloads it back, so sys.modules['cli']
is left with a mock _pt_print/_PT_ANSI and cli._cprint silently no-ops
for every later test. Fixed by reloading cli once more with the real
modules visible (try/finally).
2. prompt_toolkit's print_formatted_text caches its Output on the
process-global default AppSession the first time it renders without an
explicit output=. Under capsys (which swaps sys.stdout per test), the
first CLI test to emit through _cprint locks that cache onto its own
captured stdout, so later capsys tests read an empty buffer. Fixed with
an autouse fixture in a new tests/cli/conftest.py that resets the cached
output around each test.
Neither change touches production code or the flaky test's own assertion.
Related to #59358 (which addresses the same flaky test by mocking _cprint in
the assertion instead; this fixes the two underlying leaks at the source and
does not modify test_resume_quiet_stderr.py).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(tests): stop the suite writing into the operator's real Hermes logs
hermes_cli/main.py calls setup_logging() at module scope. That resolves
get_hermes_home() and attaches rotating file handlers to the ROOT logger via
a QueueHandler. So merely importing it - which many test modules do, directly
or transitively - points the whole pytest session's logging at
<HERMES_HOME>/logs/agent.log and errors.log.
The _isolate_env fixture already sandboxes HERMES_HOME, but fixtures run
after collection has imported the test modules, and by then the handler holds
an absolute path to the real file. Verified by importing hermes_cli.main in a
clean interpreter and walking the queue listener: both handlers pointed at the
developer's own ~/.hermes/logs/.
Measured on a live install: 126 warnings in a personal agent.log came from
test runs rather than the running gateway - phantom 'FakeTree' Discord
registration failures and 'rejected invalid API key' entries whose paths only
exist in tests/gateway/test_api_server_runs.py. That noise makes genuine
warnings hard to find exactly when someone is debugging.
conftest is imported before any test module, so sandboxing HERMES_HOME there
closes the window. The per-test fixture still applies afterwards.
Also fixes 4 pre-existing failures: tests/gateway/test_channel_directory.py
TestBuildFromSessions was reading the operator's real sessions data for the
same reason.
Full gateway+tools suites: 66 failures on clean origin/main, 62 with this
change, 0 new. The regression guard asserts the value captured AT conftest
import - reading os.environ inside a test passes even with the fix removed,
because the per-test fixture has sandboxed it by then.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(tests): tolerate mid-import kanban_db in the write-guard sys.modules probe
The guard's sys.modules.get() can observe hermes_cli.kanban_db while a
lazy import is still executing on a fixture boundary — the partially
initialized module has no .connect yet (AttributeError flake in the
full-suite verification run, 1/2460 files). A half-imported module has
no callers to guard; skip this round and let the next fixture patch the
completed module.
* feat(desktop): full cmdk-style keyboard selection in the model dropdown
Enter-commits-first was still filter-only past the first row: arrows
handed focus to Radix's own item focus, hover fought the keyboard for
which row Enter meant, and unfiltered MoA presets sat below zero model
matches as phantom commits.
The search input now owns the whole keyboard interaction over one flat
row list that mirrors exactly what's rendered (collapse, filter,
presets included — presets filter by query now too):
- no query → selection sits on the current model, Enter just closes
- typing → first match auto-selected, ↑/↓ step with wraparound (reset
on every keystroke), Enter commits the highlighted row
- selection scrolls into view; the highlight yields while the pointer
is in the list so hover and keyboard never disagree about Enter
* fix(tests): preserve config module identity in backup tests
(cherry picked from commit 1ac105ea50db6fac4a18b606a6cd9b06d7f5925d)
* fix(update): test runs never mutate the live checkout — pytest-guard the marker and repair paths
Guard the .lazy-refresh-incomplete marker writer (update_cmd), launch-time
recovery (main.py), and _early_recovery repair paths behind a two-condition
check: running under pytest AND the target is this live checkout. Sandboxed
tmp_path tests still exercise the real code paths.
Salvaged from PR #72002 by @fcavalcantirj. Fixes #72000.
Co-authored-by: fcavalcantirj <felipe.cavalcanti.rj@gmail.com>
* test: prove concurrency with barriers/witnesses instead of wall-clock bounds
Replace OS-scheduler-dependent elapsed-time ceilings with deterministic
concurrency proofs in three tests:
- test_context_refs_concurrent: asyncio.Barrier rendezvous — all 3 URL
fetches must be in flight simultaneously before any returns.
- test_memory_boundary_commit: positive non-blocking witness — the
provider call list must still be empty when the async commit returns.
- test_mem0_v3 slow-prefetch: threading.Event park/release — prefetch
must return while the backend search is still parked.
The tests/tools/test_mcp_tool.py hunk from the original PR is dropped:
main no longer carries the 'elapsed < 2.5' assertion it targeted
(superseded by a delay-relative bound).
Salvaged from #71913.
* test(tui_gateway): make the tui gateway suite order-independent
Cross-file leaks made tests/tui_gateway + tests/test_tui_gateway_server.py
fail when run in one process (issue #57068):
- conftest: _hermetic_environment now re-pins hermes_state.DEFAULT_DB_PATH
to the fake home so no test ever touches the developer's real state.db
- conftest: new autouse _reset_tui_gateway_server_state snapshots/restores
_methods, cfg cache, db handle and _real_stdout, and tears down leftover
sessions via the production _close_session_by_id(..., end_reason=
"test_cleanup") boundary
- per-file fixtures scope patch.dict(sys.modules) to the import only
- drop reload()-based teardowns that duplicated atexit hooks
Conflict resolution vs main: kept main's mod._live_transports.clear() in
the test_protocol.py server fixture alongside the snapshot/restore logic.
Combined run now 792 passed / 0 failed.
Salvaged from PR #57066 by @lEWFkRAD. Fixes #57068.
* fix(tests): register no_isolate and ssh pytest markers
Registers the two genuinely-unregistered markers (no_isolate, ssh) in
pyproject.toml, silencing PytestUnknownMarkWarning for tests/tools/test_mcp_*
and tests/tools/test_file_sync_perf.py.
Dropped hunk: the live_system_guard_bypass line from the original PR — that
marker is already registered dynamically via pytest_configure/addinivalue_line
in tests/conftest.py, so the ini entry would be a duplicate.
Salvaged from #71403.
* test(fal): pin fal_common behavioral contracts
Contract tests for tools/fal_common.py: queue-URL normalization
(trailing slash / whitespace / empty-raises), _extract_http_status
response-vs-exc precedence and non-int rejection, the
_ManagedFalSyncClient RuntimeError guards against fal_client private
API drift, and submit()'s queue-URL + POST/json/timeout wiring.
Trimmed on landing: test_non_string_coerced_to_string (pins an
implementation accident, not a contract) per the coverage-padding
policy in AGENTS.md.
Salvaged from #52166.
* fix(tests): live-system guard treats only argv[0] as the executable
Argv-list commands now scan only argv[0] against _PROCESS_KILLERS, ending
false positives like ['cat', '.../skill'] ('skill' is a real util-linux
binary name). Wrapper executables (sh/bash/env/nohup/timeout/sudo/xargs/…)
keep full-token scanning so ['bash', '-c', 'pkill …'] and
['env', …, 'pkill', …] stay blocked; string commands are unchanged.
Adds tests/test_live_system_guard.py pinning both directions.
Salvaged from PR #43299 by @eazye19.
Co-authored-by: Tony (eazye19) <support@captureclient.net>
* test(tui_gateway): pin goal-command config home against collection-time _hermes_home freeze
Sibling files (test_billing_rpc etc.) import tui_gateway.server at
collection time, freezing module-level _hermes_home = get_hermes_home()
(server.py:54) to the developer's real home before conftest isolation
runs. _load_cfg() then reads the REAL ~/.hermes/config.yaml — e.g. a
local MoA preset — instead of what _write_moa_config wrote.
The server fixture now monkeypatches _hermes_home to the isolated
HERMES_HOME and resets the mtime-keyed cfg cache (_cfg_cache/_cfg_mtime/
_cfg_path); monkeypatch restores originals on teardown.
Complementary to the #57066 autouse teardown, which only restores the
cfg cache to its pre-test value and never re-points _hermes_home.
Combined tests/tui_gateway + tests/test_tui_gateway_server.py:
792 passed.
Salvaged from PR #63981 by @lEWFkRAD.
* fix(test-runner): native Windows venv probe + glyph-safe stdio
Two Wind…
SSC-ENG
added a commit
to SSC-Engineering/hermes-agent
that referenced
this pull request
Jul 30, 2026
* fix(tests): eliminate flaky/broken tests — shadow sys.path inserts, unmocked network in compressor tests, stale-SDK feishu pin guard, quadratic redact regexes
- Remove tests/-shadowing sys.path.insert(dirname/'..') from 11 test files:
it prepended the tests/ dir itself to sys.path, so 'import agent' /
'import hermes_cli' resolved to the test packages and collection died
with ModuleNotFoundError depending on import order (2 files failed in
every full-suite run; 9 more were latent).
- Patch call_llm in 5 context-compressor tests that called compress()
unmocked: each burned ~50s attempting live LLM traffic through the
relay before falling back (572s file — the slowest in the suite, and
flaky under the 300s per-file timeout). File now runs in ~5s.
- agent/redact.py: fix two catastrophically-backtracking regexes hit by
the compressor's redaction pass on large payloads —
_STRICT_URL_USERINFO_RE anchors on the mandatory '//' (optional-scheme
prefix backtracked O(n^2): ~55s on a 320KB payload, now sub-ms;
output-equivalence fuzz-verified on 20k random strings), and the
_CFG_DOTTED_RE/_CFG_ANCHORED_RE subs gain an exact linear keyword
pre-gate so secret-free text skips the quadratic pattern entirely.
- tests/gateway/test_feishu.py: version-guard the extra_ua_tags SDK
signature check; the repo pins lark-oapi==1.6.8 but stale local
installs (1.5.3) fail the assertion — skip below the pin.
- tests/tools/test_managed_browserbase_and_modal.py: stub
agent.redact + agent.credential_persistence in the fake agent package
(empty __path__ blocks all real agent.* imports added since the fake
was written).
- tests/gateway/test_startup_restart_race.py: raise wait_for timeouts
2s -> 30s; 2s wall-clock on a loaded 40-worker box flaked in the
baseline run (passes instantly when the box is quiet).
* test: prune low-value tests suite-wide (wave 1) — 46,820 → 28,106 test functions
Systematic prune per AGENTS.md test policy, one pass over every major
test tree (gateway, hermes_cli, tools, agent, run_agent, plugins, cli,
cron, tui_gateway, honcho/openviking, root-level):
- DELETE: source-reading tests (read_text/getsource on prod files),
change-detector tests (exact catalog counts, model-name snapshots,
config version literals), mock-echo tests (assert a mock returns what
it was told), assertion-free/trivial tests, near-duplicate
parametrizations (boundaries + one representative kept), async/sync
twin duplicates, cosmetic within-file variations.
- KEEP (mandatory): security/redaction/approval guards, message-role
alternation invariants, prompt-caching/deterministic-call-id
invariants, issue-number regression tests (deduped), E2E tests.
- 6 test files deleted outright (script-style/no-assert or fully
redundant); conftest.py, fakes/, fixtures/ untouched.
- tests/acp/conftest.py added: autouse fixture stubs the live
models.dev/GitHub/Copilot/Anthropic inventory fetches that ACP server
tests performed on every session create — test_server.py 147s → 3.4s,
and the tests are now genuinely hermetic.
- Sleep-based slowness shrunk where safe (codex_ttfb_watchdog,
compression_concurrent_fork, etc.); no wall-clock assertion tightened.
Verification: full hermetic suite via scripts/run_tests.sh —
2439 files, 31,130 tests passed, 0 failed, 0 flaky retries, 315s wall
(baseline: 583s wall, 13,564s subprocess CPU).
* test: prune wave 2 + speed fixes — 28,106 → 19,757 test functions, suite wall 315s → 294s
Second, deeper pass over tools/gateway/hermes_cli plus first pass over
the trees wave 1 missed (acp, acp_adapter, skills, computer_use, docker,
dashboard, conformance, monitoring, secret_sources, hermes_state,
providers). Same rubric as wave 1 (AGENTS.md test policy); security,
alternation/caching invariants, issue-number regressions, and E2E kept.
Real test-quality fixes found and rooted out along the way:
- tests/tools/test_command_guards.py made real auxiliary-LLM HTTPS calls
(DEFAULT_CONFIG smart-approval leaked in) — pinned approval
mode=manual via autouse fixture: 17.4s → 0.4s.
- test_model_switch_custom_providers.py / test_user_providers_model_switch.py
silently probed live provider catalogs (~2s/test) — stubbed
cached_provider_model_ids/provider_model_ids/fetch_api_models.
- test_telegram_noise_filter.py: 15-platform copy-paste matrix over
shared gateway.run logic → 3 representative platforms (55s → 3.9s).
- test_gateway_shutdown.py: stop()'s 5s interrupt-deadline loop spun on
MagicMock agents — interrupt.side_effect now clears _running_agents
(22s → 1.0s).
- test_gateway_inactivity_timeout.py poll-harness timings shrunk 3-5x
(24s → 1.1s); test_mcp_stability.py backoff/SIGTERM-grace sleeps
patched (15.4s → 2.5s); test_async_delegation.py negative-drain wait
5s → 0.5s.
- test_telegram_init_deadline.py: loop-block margin restored to 1.0s
with rationale comment — the watchdog-dump assertion needs the loop
blocked well past deadline+grace under parallel load (flaked once in
the 40-worker verification run at a 0.2s margin).
Verification: full hermetic suite via scripts/run_tests.sh —
2,438 files, 21,718 tests passed, 0 failed, 293.9s wall.
Suite totals vs original baseline: 46,820 → 19,757 test functions
(−57.8%), wall 583.5s → 293.9s (−50%), subprocess CPU 13,564s → 11,623s.
* feat(desktop): platforms.changed broadcast retires the Messaging page's 6s status poll
The change watcher (#73673) missed one always-on-while-mounted timer: the
Messaging page polled /api/messaging/platforms every 6s for connection
status. The gateway already persists platform connect/disconnect/health to
gateway_state.json, so watch that file's mtime and broadcast
platforms.changed (floored to 5s — the gateway also rewrites the file for
in-flight-count bookkeeping), route it through live-sync like its
siblings, and refresh the page on the tick. Older backends keep the
legacy visible-tab poll verbatim.
Finishes the always-on poll sweep for #73618.
* fix(installer): never let a stale --commit pin roll an install backwards
hermes-setup.exe bakes its build-time commit into the binary
(BUILD_PIN_COMMIT) and passes it as -Commit on every install-mode run,
including the retry the desktop's "Update didn't finish" screen kicks
off. The repository stage checked that SHA out unconditionally, so an
installer built months earlier rewound a current managed checkout to its
build commit -- 9,160 commits in the reported case -- leaving ancient
source against a current venv. npm then failed on workspaces that did not
exist yet at that commit, and every later update ran against the wrong
tree.
Skip the pin when its target is already an ancestor of HEAD. Fresh clones
have no such ancestry so reproducible/CI pinning is unchanged, and
--force-commit / -ForceCommit still rolls back on purpose.
* fix(update): make the in-progress marker a real cross-process lock
Three surfaces start updates against one checkout: a terminal
"hermes update", the dashboard's Update button (which spawns that same
command detached), and the desktop's, which hands off to the Tauri
updater. Only the Tauri updater published the in-progress marker, and
only Electron read it -- to gate backend startup, not to stop a second
updater. So a dashboard-spawned update and an installer-driven git
checkout could mutate the same tree concurrently, rewriting source under
a live interpreter.
Claim the same marker from cmd_update rather than adding a second
mechanism: same path, same pid+started_at payload the Rust and Electron
readers already parse. A marker only counts as live when its pid is alive
and it is inside the shared age ceiling, so a crashed updater self-heals
instead of wedging every future update. Release only removes a marker we
still own, leaving a handoff partner's claim intact.
Refusing exits 2, matching the existing concurrent-instance contract the
Tauri updater already recognizes.
* fix(installer): refuse a second updater instead of clobbering the marker
UpdateMarkerGuard::acquire overwrote the in-progress marker
unconditionally, so a Tauri update launched while a dashboard-spawned
"hermes update" was mid-flight simply took the marker and ran a second
updater over the same checkout. That is the race behind the reported
Windows failure: install-mode bootstrap rewound the tree while the
dashboard's updater was still running npm install against it.
acquire now returns Result and refuses when a live foreign owner holds
the marker, and Drop no longer deletes a marker this process does not
own. Liveness matches the Python and Electron readers of the same file:
dead pid or past the shared age ceiling means stale and reclaimable, so
a crashed updater cannot wedge future updates.
Adds a cfg(unix) libc dependency for the signal-0 liveness probe; the
Windows path uses OpenProcess/GetExitCodeProcess.
* fix(installer): release the update marker on success, not just on drop
The updater keeps a Tauri/Cocoa event loop alive while it relaunches the
desktop, and that loop can outlive app.exit(0). Relying on Drop alone
left a *successful* update looking active -- a live pid holding a fresh
marker -- which blocked desktop startup and, now that the marker is also
the cross-process update lock, every subsequent updater until the age
ceiling expired.
Release explicitly once all install-tree mutations are done, before the
relaunch. complete() is idempotent so Drop still covers the failure and
panic paths. Arms a process-exit fallback so a wedged event loop cannot
leave a finished updater lingering as a live pid.
Co-authored-by: nateEc <nateEc@users.noreply.github.com>
* fix: approve listed pairing requests
* test: cover pairing CLI approval paths
* fix(pairing): reset the failed-approval counter on a successful approval
approve_code()'s success path never cleared _failures:{platform}. The
counter is incremented on every non-matching code, persisted in
_rate_limits.json, and only ever reset to 0 when it reaches
MAX_FAILED_ATTEMPTS (firing the lockout). So it counts failures over the
gateway's entire lifetime, not consecutive ones.
An owner who mistypes a pairing code on a handful of separate occasions
— each time immediately retyping it correctly and successfully pairing —
accumulates those isolated typos. A later single fresh typo then hits
MAX_FAILED_ATTEMPTS and locks the whole platform out for an hour, at
which point _is_locked_out gates approve_code and even the *correct*
code is rejected.
Reset the counter on a successful approval, matching standard
brute-force-guard semantics (the counter tracks consecutive failures).
This does not weaken protection: an attacker cannot produce a success
without a valid code, and 5 consecutive wrong attempts still lock out.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(pairing): keep GUI approvals off the code brute-force lockout
Follow-up hardening on the request-id grant path.
approve_request took the same lockout treatment as approve_code: gated by
it, and recording a miss toward it. But the two paths defend different
things. The lockout exists to stop guessing at the 8-char code space over a
messaging channel; a request id is only ever obtained by an admin already
authenticated to the store, so a miss means the row they clicked went stale.
Counting those let a handful of clicks on a stale list lock the operator out
of `hermes pairing approve` for an hour — the GUI DoSing the CLI.
Also drops the `code`/`code_hash_prefix` compat fields from list_pending.
The hash prefix is what admin surfaces mistook for an approvable code in the
first place, and re-exporting the request id under the old `code` key just
preserves the ambiguity; both consumers in the tree read `request_id` now.
The 16-hex sniffing that had been copy-pasted into the CLI and the endpoint
(where a chained conditional consulted it against the wrong field) moves to
one owner, PairingStore.looks_like_request_id.
The endpoint no longer reports a 429 on the request-id path, where lockout
can't apply — a stale id surfaced as a bogus "locked out" while the platform
sat locked for something else entirely.
* fix(update): use the no-kill pid probe, not os.kill(pid, 0)
The Windows-footgun linter caught a real bug in the new update lock. On
Windows os.kill(pid, 0) is not a no-op: CPython routes sig=0 to
GenerateConsoleCtrlEvent, which sends Ctrl+C to the target's entire
console process group (bpo-14484). The liveness probe would have killed
the very updater it was asking about -- and any sibling sharing that
console.
Delegate to gateway.status._pid_exists, the project's existing no-kill
probe, which uses psutil (OpenProcess/GetExitCodeProcess on Windows) and
also reports zombies as dead. Any pid we cannot evaluate still counts as
dead so a corrupt marker cannot wedge the lock.
* fix(desktop): stop an unowned publisher poisoning the thread clearance at :root
The thread's bottom clearance is composer + status-stack + 2rem, and both
inputs are measured by JS onto the owning [data-chat-surface]. The surface-var
helpers fell back to document.documentElement when they couldn't resolve one —
but :root is where every surface's DEFAULTS live, so a single stale write there
becomes a global floor under every thread's clearance until reload.
An unowned publisher has nowhere to publish to, so it now publishes nowhere.
* refactor(desktop): move the main-is-occupied check into the session door
newSessionOpensTab answers a question that isn't specific to the sidebar
"+" — is there a conversation on main that must not be discarded — and the
palette needs the same answer. Fold it into open-session as
mainChatOccupied so both callers share one definition.
* feat(desktop): find and reuse an open blank "New session" tab
An unused draft tab is the one a user would have typed into, so give the
tile store a way to name it (blankDraftTile) and hand it to another
session in place (reuseBlankDraftTile). A blank-but-busy tab has its
first turn in flight and an unbound tile is unknown rather than empty, so
neither is a candidate.
* fix(desktop): stop ⌘K and notification clicks stealing the main tab
Both surfaces passed the sidebar's in-place intent, which means "load it
into main when it isn't already on screen" — right for a row you clicked
in a list you were looking at, wrong for a chat opened from outside the
workspace. Neither had a surface of its own, so they took the one you
were using.
Add a stack intent for that case. It focuses the session when it's
already open, spends an unused draft tab when there is one, and only
falls back to main while main is itself a blank draft. Modifiers still
force a tab or window.
* feat(desktop): tab verbs follow the pane under the pointer
⌘1…⌘9, ⌃Tab, and the ⌘W / ⌘T family all resolved the last-interacted
zone, so switching tabs in a second pane meant clicking into it first.
They now resolve the HOVERED zone when the pointer is in one, falling
back to the focused zone otherwise — hover pane 1, ⌘2; hover pane 2, ⌘2,
and each lands in its own strip.
tabTargetGroupId() is the single resolver, keeping the number keys and
the tab verbs from disagreeing about which zone is "the" zone the way
they already couldn't. pointerover fires per boundary crossing rather
than per mouse move, and leaving the document clears the override so a
parked pointer never strands the keys on a stale zone.
* refactor(gateway): extract run_sync onto TurnRunner (completes the TurnContext seam; AST-identical body)
* refactor(web): extract sessions/mcp/skills/tools routes to APIRouter modules (wave 2; route-table equality verified)
- hermes_cli/web_routers/sessions.py: 14 routes across 3 routers
(list_router, search_router, manage_router) mounted at the three original
registration points so global route order is preserved exactly.
- hermes_cli/web_routers/mcp.py: 11 routes; OAuth flow registry
(_mcp_oauth_flows/lock/cap) stays in web_server, reached via new
web_deps.LateState live proxies so tests mutating web_server._mcp_oauth_flows
keep working.
- hermes_cli/web_routers/skills.py: 12 routes across hub_router + router
(two original registration points straddle the profiles router include).
- hermes_cli/web_routers/tools.py: 12 routes; toolset/terminal catalogs stay
in web_server (some are defined after the mount point), reached via LateState.
- web_deps.py: add LateState — operation-time proxy for web_server-owned
module state (getattr/item/iter/len/contains/context-manager/comparisons).
- Handler bodies byte-identical; legacy re-exports keep
web_server.<handler> importable for tests.
- Verified: ordered route table (method, path) identical to pre-refactor app
(291 routes); import smoke; ruff; windows-footguns clean.
- test_web_server_sessiondb_eventloop.py: structural AST scan now reads both
web_server.py and web_routers/sessions.py (handlers moved; helpers stayed).
* feat(desktop): approve pairing requests from the messaging page
Desktop had no pairing surface at all. Someone DMs the bot, gets a code,
and lands in pending — where only the dashboard or `hermes pairing approve`
could let them in. That gap is why the dashboard's broken approve button
went unnoticed for so long: desktop users never saw the flow.
This puts it where the decision already lives. The messaging page is
already master-detail by platform, already polls every 6s, and already owns
"who can talk to this bot" — the allowlist env vars in the same detail pane
are what an approval writes into. A pending block sits above the credential
fields (approving is the hot path); a count badge on the platform row is the
discovery mechanism, so you see "Telegram 2" whenever you open Messaging for
any reason. Neither renders when nobody is waiting: approvals are rare, and
a permanent empty state would be chrome on a page that is otherwise about
credentials.
Approval sends the row's request_id and never a code — the code is the
requester's proof that the channel is theirs and is never returned by the
API. Rows paint optimistically from a snapshot and roll back visibly on
failure, and a 429 gets its own message since the code path's lockout is a
condition the operator can only wait out.
Pairing rides the existing platform refresh via allSettled, so a backend
without the endpoint yields no rows instead of blanking the page.
* style(desktop): sort the confirm-dialog import (eslint perfectionist)
* fix(gateway): scope pairing platform discovery to the profile dir
The per-profile pairing isolation added self._dir and scoped every
per-file path helper (_pending_path, _approved_path) to it, but
_all_platforms still enumerated the module-global PAIRING_DIR. For a
profile-scoped PairingStore, list_approved/list_pending/clear_pending
therefore operated on the GLOBAL platform set while loading each
platform's file from the PROFILE dir — so list_approved() returned []
for a user that is_approved() confirmed as approved, a silent divergence
between the authz surface and the list/inspect/clear surface.
Route discovery through self._dir. Byte-identical for the global store
(self._dir == PAIRING_DIR when no profile is set); only the buggy
profile-scoped case changes. self._dir is guaranteed to exist (__init__
mkdirs it).
* test(gateway): patch hermes_constants.get_hermes_home so profile store scopes to the mocked home
* fix(gateway): align multiplex pairing stores
Co-authored-by: x7peeps <x7peeps@users.noreply.github.com>
* fix(pairing): scope the approve/revoke endpoints to a profile
The gateway keeps one PairingStore per served profile, but every
`/api/pairing` endpoint built the global one. An operator managing a named
profile saw the wrong pending list, and approving wrote a grant into a
whitelist their running gateway never consults — the user stays locked out
while the UI shows them as approved.
`_pairing_store(profile)` now resolves per profile and validates the name
(400/404 on an unknown one). No `_profile_scope` needed: PairingStore
resolves the profile's home itself, so nothing process-global is swapped
across an await.
Both GUIs had to change to match. The listing rides the query param — for
the dashboard that meant deleting `pairing` from the "machine-global, must
NOT be rewritten" exclusion list, a comment this change makes false. The
mutating endpoints read the profile off the BODY, which no query-param
rewrite reaches, so approve/revoke send it explicitly on both surfaces.
* refactor: config auto-migration support floor at v12 + deprecated shim retirement
* fix: floor gate only refuses configs with an EXPLICIT below-floor _config_version
Profile clones and hand-written minimal configs carry no _config_version
key; check_config_version() coerces that to 0, which wrongly tripped the
v12 floor and left clones unstamped (caught by CI:
test_clone_config_copies_files / test_clone_from_named_profile).
Version-less configs now take the normal ladder + fresh stamp — the
historical behavior; only genuinely ancient explicit-version configs
are refused.
* fix(desktop): treat landed memory writes as success, not warnings
Trust explicit success over a stale isError envelope so real saves stop
painting amber. Over-budget refusals keep a soft warning with "Memory
write noted" instead of crowing "Saved", and entry_count labels as
entries.
* style(desktop): gold→purple chrome on landed memory saves
Paint successful memory tool rows with a gradient title, tinted brain
glyph, and purple meta so a save reads prized rather than like a warning.
* feat(pairing): give pairing its own change signal
The poll this page's pairing block rode was retired hours earlier by
f8e07a332, which moved Messaging onto platforms.changed. That signal is the
mtime of gateway_state.json — where the gateway persists connect/disconnect
health — and a new pairing request moves none of it. So on an event-capable
backend a pending row stayed invisible until something unrelated
reconnected, and the count badge with it.
Adds pairing.changed to the change watcher, signed off the pending/approved
ledgers across the global store and every profile's own. _rate_limits.json
is deliberately excluded: it moves on every unauthorized DM, including ones
that produce no new row, so signalling on it would refetch for nothing.
The page now refreshes platforms and pairing on their own signals rather
than one combined call, and the legacy visible-tab poll (older backends)
covers both.
* fix(desktop): stop composer chips demoting to plaintext on every re-render
Two halves of one bug class: the composer treated 'rebuild the editor
from serialized text' as routine, and serialized text couldn't express a
slash pill.
In-place commits: Chromium fragments text nodes around
contenteditable=false chips, so the old commit path — whole token in ONE
text node with the caret at its end — failed almost every keyboard pick
and fell into the rebuild fallback. rangeBeforeCaret now walks the token
backwards across sibling text nodes and refuses only at real boundaries
(chip, <br>, block), making in-place replacement the default for picks,
folder descends, Backspace path-ascends, and action items in both the
main composer and the user-edit composer.
Slash-pill hydration: renderComposerContents re-chipped @kind:value refs
but never /command, so any surviving rebuild (draft restore, undo, the
fallback above) demoted a committed command pill. A leading no-arg
/command now hydrates back to its pill; arg-taking commands stay text
since their tail may be uncommitted prose.
Also: popover picks bank an undo point in the main composer, and Tab is
swallowed while completions are in flight instead of moving focus out of
the composer.
* fix(managed_uv): repair vulnerable SQLite runtime in .venv installs too
repair_vulnerable_runtime() hardcoded <checkout>/venv as the live venv,
so uv-default/dev checkouts installed into .venv got 'not-applicable' on
every hermes update — no repair path ever fired, leaving state.db-class
DBs on journal_mode=DELETE forever (measured 26 ms + ~5.5 fsyncs per
append vs ~0.01 ms under WAL, ~2,600x) while the WAL fallback warning
falsely promised hermes update would repair the runtime.
- _default_live_venv(): target venv/ when it has an interpreter (managed
layout precedence), fall back to .venv/, keep not-applicable when
neither exists. Explicit venv_dir arg unchanged; all staging/smoke/
cutover/rollback machinery untouched.
- Rebuilt against the pruned test suite (main's test-prune waves 1+2
rewrote test_managed_uv.py, so this reapplies cleanly): 3 new
TestDefaultLiveVenv tests + repair neutralized in the 6 unit tests
whose subject is uv install/self-update mechanics — with .venv now
probed for real, CI's own vulnerable .venv made the unmocked repair
hook fire inside those tests and re-invoke _install_uv.
33/33 tests green on the pruned suite.
* fix(desktop): keep chips atomic to composer trigger detection
textBeforeCaret serialized chip labels into the string the trigger
regexes scan, so a leading /work pill made the anchored command regex
swallow the rest of the line as its argument — silencing the @ popover
for the whole message. Chips now contribute an object-replacement
placeholder and <br> a newline, so committed pills can't poison
detection.
* test(desktop): cover the composer chip plaintext-demotion bug class
Backspace path-ascend keeping the leading command pill, folder picks
alongside a command pill, commits spanning Chromium-split text nodes,
slash-pill hydration boundaries (committed vs half-typed vs arg-taking),
and replaceBeforeCaret refusing across chip boundaries.
* fix(telegram): batch near-limit command chunks so split /queue pastes don't orphan their continuation
Telegram clients split messages above 4096 chars into multiple updates. A
long '/queue <prompt>' paste arrives as a COMMAND chunk near the limit plus
plain TEXT continuation chunk(s). _handle_command dispatched the command
chunk immediately, so the continuation landed as a separate plain message
that interrupted the running agent instead of being queued.
Near-limit (>= _SPLIT_THRESHOLD) command chunks now route through the same
text-batching pipeline used for split plain-text messages, merging the
continuation before dispatch. Short commands (/stop, /approve, ...) keep the
immediate path and are never delayed.
* fix(state): time-based write-lock patience so busy sibling processes can't destroy turns
A shared state.db is legitimately held for multi-second stretches by
sibling Hermes processes: VACUUM after auto-prune, the TRUNCATE WAL
checkpoint at close on a large WAL, offline recovery, or an older
still-running process whose FTS maintenance predates the bounded-merge
protocol (every `hermes update` leaves mixed-version processes sharing
the DB until the old ones exit).
The old retry budget was attempt-counted: 15 attempts x 20-150ms jitter
gives up after ~1.3s of waiting. Any hold longer than that surfaced as:
- append_message failing -> the conversation loop aborts the turn as
session_persistence_failed ('No reply: the turn was stopped because
session storage could not be written') on a perfectly healthy store;
- SessionDB() open failing -> the CLI disables persistence for the
entire run ('Failed to initialize SessionDB ... database is locked').
Both observed in production logs on 2026-07-29 (10.8 GB state.db, 9
concurrent hermes processes, three of them pre-dating the bounded-merge
fix pull).
Changes:
- _execute_write patience is now TIME-based with two budgets: routine
writes wait up to 20s; transcript-critical writes (append_message,
session-row creation — the ones whose failure aborts a user turn)
wait up to 60s. Jitter stays 20-150ms for the first 2s, then backs
off to 250ms-1s so a long hold isn't hammered with BEGIN IMMEDIATE.
- Exhausted patience raises an error that names the actual cause
(another process held the write lock; the database is healthy)
instead of a bare 'database is locked' that reads like disk damage —
and the turn-abort explainer inherits that clarity.
- SessionDB open now applies the same jittered patience to the
locked/busy class around connect+schema-init, instead of failing the
whole open (and disabling persistence for the run) on the first 1s
timeout. Non-lock errors, including the malformed-schema repair
class, propagate immediately as before.
Fixes #74478
* fix(gateway): relay TTS attachments + semantic auto-thread rename on the title turn (#74482)
Two relay-lane bugs from live Discord staging testing (2026-07-29):
1. TTS audio never attached over relay (any platform, any lane).
_history_media_paths_for_session excluded only the trailing assistant
entry from the persisted transcript when building the delivered-media
dedup set. The agent persists rows as it produces them, so THIS turn's
text_to_speech tool result (media_tag JSON) was already in the
transcript at delivery time — the fresh TTS path deduped against
itself and extract_media's attachment was silently stripped
(response_delivery_dropped for a MEDIA-tag-only reply; fly logs show
the exact signature). Fix: exclude everything from the last USER
message onward (the current turn); prior-turn dedup unchanged.
Affects every platform adapter (native + relay) on the non-streaming
delivery path — the streaming path passes explicit history and was
unaffected.
2. Connector-auto-created threads never got the LLM session-title
rename. The title fires on the FIRST exchange, whose source is the
PARENT channel event — the thread didn't exist at ingest, so the
Phase 4 auto-thread markers can't be present and
_is_discord_auto_thread_lane never matches on the relay title turn
(initial titles worked; semantic renames never happened; staging
telemetry shows zero thread_rename ops ever sent). Fix: consume the
connector's new send-result feedback (paired gateway-gateway PR —
contract §SendResult thread_id/auto_thread_name, additive):
RelayAdapter.send() caches (thread_id, initial_name) per chat
(bounded 256), run.py's title-callback registration + rename lane
read it back and pass initial_name as only_if_current_name so the
human-rename-wins guard holds on the relay lane too. Native marker
path unchanged; connectors that don't stamp the fields degrade to
exactly the old behavior.
Tests: 3 new (send-result feedback capture, absence, bound) in
test_relay_threads.py; 3 new in test_history_media_current_turn.py
(current-turn TTS not deduped, prior-turn still deduped, no-user-row
fallback). Relay suite 144 passed.
* fix(state): configurable journal_mode + centralize all DB openers
Add HERMES_JOURNAL_MODE env / database.journal_mode config for
virtiofs/NFS/SMB where WAL is not crash-safe. Route 5 bypass openers
through apply_wal_with_fallback so a single setting covers every .db
(#68545).
* fix(state): restore async_delegation.py symbols, keep only journal_mode routing
The previous commit accidentally reverted async_delegation.py to a
pre-origin_session_id snapshot, dropping _MAX_DELIVERY_ATTEMPTS,
_current_origin_session_id, _transaction(), the origin_session_id
column, and drop_completion_delivery() — causing ImportError in
delegate_tool.py and kanban_tools.py.
This restores the upstream main version of async_delegation.py and
re-applies only the journal_mode routing change (db_label update to
'async_delegation.db').
Addresses reviewer feedback on #68912.
Signed-off-by: Jasmine Naderi <jasmine@smfworks.com>
* fix(state): make journal mode canonical and behaviorally verified
Use database.journal_mode as the sole non-secret operator setting, preserve the vulnerable-SQLite safety gate and existing WAL databases, validate explicit DELETE results, document the active config path, and cover real SQLite openers with behavioral tests.
* fix(gateway): add disk I/O error to WAL-fallback markers for ZFS (#55305)
* fix(state): detect silent WAL→DELETE fallback on macOS NFS / SMB (no false "wal")
apply_wal_with_fallback() only detected WAL-incompatible filesystems via a
RAISED OperationalError matched against _WAL_INCOMPAT_MARKERS. But macOS NFS,
SMB/CIFS, and overlay filesystems (e.g. AgentFS's NFS-backed mount) refuse the
WAL switch WITHOUT raising: `PRAGMA journal_mode=WAL` returns the still-effective
mode ('delete') and no exception. The code then ran `return "wal"`
unconditionally, so it:
1. returned a false "wal" while the DB was actually in DELETE mode, and
2. never called _log_wal_fallback_once, so the operator got ZERO signal that
concurrency had silently degraded (reader-blocks-writer).
state.db and kanban.db share this path, so a session/kanban board DB on a
network or overlay filesystem ran in DELETE with no diagnostic.
Fix: read the row `PRAGMA journal_mode=WAL` returns and verify it is actually
'wal' instead of assuming success; on a silent no-op, emit the existing
fallback WARNING and return the true mode. The raise-based path is unchanged.
Reproduced on a real AgentFS NFS overlay (PRAGMA journal_mode=WAL returned
('delete',) with no OperationalError). Adds a regression test for the
silent-no-op shape; the 16 existing WAL-fallback tests are unchanged.
* fix(state): escalate silent WAL→DELETE fallback to ERROR + opt-in require_wal
The WAL→DELETE fallback on WAL-incompatible filesystems (NFS / SMB / FUSE /
the AgentFS NFS overlay) was logged at WARNING, treating a real loss of
concurrency — under the kanban dispatcher + workers a write blocks readers,
surfacing as SQLITE_BUSY — as if it were cosmetic. Escalate the deduplicated
fallback log to ERROR so the degradation is observable, not silent.
Add an opt-in require_wal=True to apply_wal_with_fallback that raises a typed
WalUnsupportedError (subclass of sqlite3.OperationalError, so existing DB-init
handlers still catch it) instead of degrading to DELETE, for callers that
mandate WAL concurrency. All four current callers keep the default
require_wal=False so NFS-homed installs keep working unchanged.
Tests: 4 new require_wal cases; WARNING→ERROR assertion updates in both
test_hermes_state_wal_fallback.py and test_kanban_db.py.
* test(state): prove silent WAL fallback in callers
* fix(state): retry transient disk I/O errors before WAL fallback
Salvages #55322 (ZFS 'disk i/o error' marker) without regressing the
Bug D transient-EIO protection from 5c49cd0ed0. 'disk i/o error' is
ambiguous: deterministic on ZFS/APFS-CoW SHM corruption (#55305,
#71498) but often a one-shot transient (page-cache pressure, lock
contention). A blind marker match re-introduced the mixed-journal-mode
corruption pattern; a blind re-raise wedged state.db on ZFS.
Disambiguate: retry the WAL pragma twice with a short backoff. A
transient EIO clears and WAL proceeds; a deterministic failure keeps
raising and falls through to the guarded DELETE fallback (still
refusing to downgrade a DB whose on-disk header reports WAL).
Tests: transient-EIO recovery, persistent-EIO fallback, and the
never-downgrade-WAL-on-disk guard.
* fix(config): respect database.journal_mode from config.yaml
Config-driven `journal_mode`, `wal_autocheckpoint`, and `journal_size_limit`
are now honored in `SessionDB` init. Users running SQLite on NFS/SMB or
with custom tuning had no supported config surface; values were hardcoded
at connection time.
What
- New `apply_database_pragmas()` in `hermes_state.py`
- Reads nested `database:` keys from `config.yaml` via existing `cfg_get`/`load_config`
- Called after `apply_wal_with_fallback()` in `SessionDB._connect_and_init()`
Fix
- Adds optional PRAGMA switches for journal_mode, wal_autocheckpoint, journal_size_limit
- On Darwin; Windows keeps DELETE unless config explicitly requests WAL
Runtime Proof
$ /opt/homebrew/bin/pytest tests/test_hermes_state.py::TestApplyDatabasePragmas -q
3 passed in 0.72s
Regression Checks
- Full `tests/test_hermes_state.py`: 306 passed in 11.32s
* fix(state): reconcile salvaged WAL fixes with current main
- apply_database_pragmas: journal_mode ownership stays with
apply_wal_with_fallback/resolve_journal_mode (single guarded owner);
the helper now only applies wal_autocheckpoint / journal_size_limit,
via load_config_readonly (hot-path safe).
- Silent-refusal WAL success path re-applies the macOS
checkpoint_fullfsync barrier and synchronous=FULL enforcement.
- Test doubles updated for connect_tracked's factory kwarg and the
WAL-reset vulnerability gate (fixed-SQLite assumption made explicit).
* chore: map salvaged contributor emails for attribution
* fix(config): register WAL sizing pragmas in DEFAULT_CONFIG
database.wal_autocheckpoint / database.journal_size_limit are now real
schema keys (default None = SQLite defaults) so the dashboard config
schema doesn't produce a single-field 'database' category, and the two
pragmas apply_database_pragmas reads are discoverable/documented.
* fix(cli): remove venv.stale.runtime marker after successful managed runtime repair
* fix(cli): sweep aged venv.stale.runtime-* backups on hermes update
Follow-up to the salvaged success-path removal: installs that already
repaired (or predate the cleanup) still carry leaked ~1 GB parked venvs.
When the runtime probes safe, reclaim aged (>1h) stale markers next to
the live venv — age-gated to avoid racing an in-flight sibling repair,
boundary-checked via _remove_tree so symlinked names can't escape the
checkout. Also drop the now-stale 'before removing the parked venv'
user guidance in update_cmd.
Tests: success-path removal, safe-path sweep (aged removed, fresh kept).
* chore: map webtecnica contributor email for attribution
* fix(gateway): preserve explicit send-image requests from session-wide MEDIA dedup
Closes #73771
The session-wide MEDIA dedup in (base.py)
filtered ALL media paths against prior-turn history, including explicit
MEDIA: tags the model deliberately included in its response text. When a
user asked the agent to resend an image, the dedup silently swallowed it
because that path already existed in the session transcript.
The dedup is already handled correctly by the auto-append path in
(run.py), which scopes its scan to the current turn and
filters against via .
The base.py filter was redundant for auto-appended tags and harmful for
explicit ones.
Fix: remove the dedup filter on in base.py while preserving
the variable for the dedup (bare file
paths, which lack run.py protection).
* fix(gateway): widen explicit-MEDIA resend fix to the streaming path + log bare-path suppression
Companion to the cherry-picked #74158 fix (non-streaming path):
- gateway/run.py: remove the identical history-dedup filter from
_deliver_media_from_response — the post-stream rescan is explicit-only
by design (#20834), so every MEDIA tag it finds is a deliberate
attachment request; drop the now-unused history_media_paths parameter
and its call-site plumbing.
- gateway/platforms/base.py: log suppressed bare local file paths on the
surviving local_files history dedup (#73771 observability ask).
- tests: focused regression file covering explicit resend delivery on
both lanes, current-turn tool-echo poisoning, surviving bare-path
dedup + its log line, and the upstream auto-append dedup invariant.
Fixes #73771
* test(tests): assert SessionDB timeout without wall-clock
Replace elapsed-time checks with captured Future.result(timeout=...) values so the hang regression stays deterministic under parallel CI load.
* fix(tests): restore original module identities after vision-routing reloads (#61597)
_fresh_modules() in test_vision_routing_31179.py deleted agent.image_routing
(+siblings) from sys.modules and never restored the ORIGINAL modules — the
reloaded copies leaked. Any later same-process test holding function refs to
the original module (test_image_routing.py) then patched the reloaded copy
via mock.patch string targets, making patches invisible: order-dependent
failures (repro: 31179 file first → 2 failures; reversed → green).
Autouse fixture now snapshots the affected sys.modules entries and restores
them on teardown. Both orders + solo runs verified green.
Masked by the per-file-isolated canonical runner; bites anyone running
pytest tests/agent/ directly.
* test(approval): event-based waits for blocking-approval E2E polls (PR #63522 port)
Manual port of @jethac's #63522 onto the pruned tree: 3 of the original
6 fixed-budget poll sites survive (3-concurrent-agents wait, session-A/B
routing wait, two-session queue wait). Replaced each 2.5-5s hard-ceiling
poll loop with the PR's _wait_until(predicate) helper — generous 30s
ceiling reached only on genuine failure, instant return on the green
path, assert with message instead of silent fall-through.
Dropped: the 3 hunks targeting pruned code, and the unrelated
scripts/release.py mailmap hunk (AUTHOR_MAP is frozen; contributor
mapping handled via contributors/emails/).
* fix(test): hermetic env-detection tests — pin container/supervisor/HOME probes (#422)
The restart-routing, systemd-support, and subprocess-HOME tests asserted
branch behavior but left part of the real probe surface unmocked, so they
fail when the suite itself runs inside a container (self-hosted CI) or a
launchd-descended shell:
- /restart routing tests: the handler also consults the real /.dockerenv —
extract the inline probe to gateway.restart.is_container_restart_context()
(patchable seam, no behavior change) and pin it False; scrub ALL four
supervisor env markers (ambient XPC_SERVICE_NAME on macOS flipped one).
- supports_systemd_services tests: pin shutil.which('systemctl') and
is_container() so the test asserts the branch, not the host.
- copilot ACP real-HOME test: pin is_container() (auto mode prefers profile
home in containers) and scrub ambient HERMES_REAL_HOME/TERMINAL_HOME_MODE.
97 tests green on macOS dev box AND inside a docker CI runner container.
Co-authored-by: Kyzcreig <9063726+Kyzcreig@users.noreply.github.com>
* Refresh on upstream/main: resolve conflicts (no behavior change)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(tests): forward Windows location vars through the hermetic runner; patch Path.home() in hindsight _clean_env
Combines the Windows-hermeticity cluster (#67512 by @webtecnica, earliest;
#71112 by @Sanjays2402; #67196 by @anatolijlaptev1991-ctrl) into one fix:
- scripts/run_tests.sh: env -i forwarded only HOME, but native Windows
CPython resolves Path.home() from USERPROFILE (or HOMEDRIVE+HOMEPATH),
stdlib paths from LOCALAPPDATA/APPDATA, ssl/sockets need SYSTEMROOT,
tempfile needs TEMP/TMP — the strip broke collection tree-wide on
native Windows (issues #67385, #70813). Location vars (never
credentials) are now forwarded, each only when actually set, so
POSIX runs are byte-for-byte unchanged (probe-verified both ways).
PYTHONUTF8=1 added for legacy-codepage consoles printing the
runner's glyphs.
- tests/plugins/memory/test_hindsight_provider.py: _clean_env patched
HOME only; on Windows Path.home() ignores HOME. Now patches
Path.home directly into tmp_path (from #67196).
Not ported: #71112's guard test — it regex-reads run_tests.sh source,
which the test policy bans (never read source code in tests).
Fixes #67385. Fixes #70813.
* fix(test): patch _launch_configured_cwd in completion tests for hermeticity (#70041)
tui_gateway/server.py freezes _hermes_home at import time, so
_launch_configured_cwd() reads the developer's real config.yaml even
under the per-test HERMES_HOME redirect. Any absolute terminal.cwd in
the real config made _completion_cwd() ignore monkeypatch.chdir and
broke the completion tests on a pristine checkout.
Patch _launch_configured_cwd to None in the autouse _reset_fuzzy_cache
fixture and add a regression test pinning that _completion_cwd resolves
via os.getcwd() under tests.
Salvaged from PR #70148 by @smfworks (rebased onto the pruned suite).
* fix(session): resolve default state DB path at call time
DEFAULT_DB_PATH in hermes_state.py is computed at import time, freezing
the developer's real ~/.hermes even when a test fixture (or runtime
profile switch) later redirects HERMES_HOME. Any default SessionDB() —
e.g. gateway SessionStore — then opened the real state.db.
Add _default_db_path(): resolves get_hermes_home() fresh at call time,
while a deliberately re-pointed DEFAULT_DB_PATH (the established
monkeypatch escape hatch) still wins via an import-time snapshot
comparison, preserving existing test behavior. SessionDB.__init__ and
session_search's requirement check now use the resolver; explicit
db_path arguments are untouched.
Reimplemented from PR #11875 by @JorkeyLiu (original diff predates the
hermes_state rewrite); regression test ported and modernized.
* test(tui): scope close_race orphan-cleanup assert to own session key
test_session_create_close_race_does_not_orphan_worker asserted the
process-global len(unregistered_keys) >= 1: a leaked _build thread from
another session.create test in the same shard can append a foreign key
and falsely satisfy the assert. Scope both the wait loop and the assert
to this test's own stored_session_id, matching the own-key scoping the
no-race companion test already uses for the same flake class.
Salvaged from PR #46189 by @AIalliAI (rebased onto the pruned suite).
* fix(test): fail-closed kanban write guard prevents real HERMES_HOME pollution (#69283)
Autouse conftest fixture patches kanban_db.connect to refuse writes whose
resolved DB path lands under the REAL kanban root (captured at conftest
import time, before fixtures rewire the environment). Deny-list, not
allow-list, so hermetic tests moving HERMES_HOME to sibling tempdirs are
unaffected. Lazily attaches only when hermes_cli.kanban_db is already in
sys.modules.
Salvaged from PR #69385 by @smfworks; rebased by hand onto the pruned
conftest and adapted to guard on the resolved DB path (explicit db_path
or kanban_db_path()) rather than kanban_home() alone.
Co-authored-by: Jasmine Naderi <jasmine@smfworks.com>
* test(cli): remove importlib.reload seam from web_server session-token tests (#38034)
Extract _resolve_session_token() in hermes_cli/web_server.py so tests can
exercise token resolution directly instead of importlib.reload(ws), which
re-executed the whole module mid-suite (fresh FastAPI app + token) and
split module identity between test and app state.
Salvaged from PR #39038 by @rodboev (maintainer-endorsed direction);
rebased onto the rewritten test_web_server.py — dropped the PR's hunks for
test_falls_back_to_random_token's old body (test deleted in the prune,
re-added here in the PR's new form).
Co-authored-by: Rod Boev <rod.boev@gmail.com>
* test: guard browser and Keychain side effects suite-wide (#35404)
Re-port of PR #35464 onto the rewritten hermetic conftest:
- autouse _neutralize_webbrowser fixture records open/open_new/open_new_tab
and webbrowser.get() instead of launching a real browser
- autouse _neutralize_macos_keychain_creds defaults the Anthropic Keychain
reader to None, with an opt-in allow_macos_keychain marker
- regression tests in tests/test_hermetic_side_effect_guards.py
- tests/agent/test_anthropic_keychain.py opts in via pytestmark
Salvaged-from: #35464
Co-authored-by: y0shualee <yuxiangl490@gmail.com>
* fix(honcho): network-hermetic unit tests + lazy async writer start
Re-port of PR #67576:
- plugins/memory/honcho/session.py: start the async writer thread lazily on
first enqueue via _ensure_async_writer (idempotent, lock-guarded) instead
of eagerly in __init__; shutdown tolerates a never-started thread
- tests/honcho_plugin/conftest.py: package-wide socket guard so no honcho
unit test can reach a live server
- tests/honcho_plugin/test_network_isolation.py: regression tests
- test_async_memory.py / test_oauth_flow.py: rebased hunks onto the pruned
suite (pruned tests not resurrected)
Salvaged-from: #67576
Co-authored-by: eapwrk <eapwrk@gmail.com>
* test: opt install-ladder tests back into lazy installs under the hermetic gate
The HERMES_DISABLE_LAZY_INSTALLS=1 conftest gate (from #43782) correctly
blocks real mid-run pip installs suite-wide, but
TestInstallDependenciesRunner exercises the install ladder itself against
a fully mocked subprocess.run — it needs the gate open. Same
both-directions override pattern tests/tools/test_lazy_deps.py already
uses. Sibling sweep of all install_specs/_pip_install/ensurepip test
files: 274 tests green.
* chore: contributor mapping for eapwrk (honcho salvage, #67576)
* fix(desktop): stop cold-start resume homing focus to the workspace tab (⌘R tab persistence)
The layout tree persists the active tab, but every reload landed on main
anyway: boot's route resume sets $selectedStoredSessionId, and the
selection listener in store/session-states.ts treats every selection
change as a navigation — noteActiveTreeGroup(null) +
revealTreePane('workspace') — fronting the workspace tab over the
persisted active tile and then persisting that clobber.
A cold-start restore is a re-attachment, not a navigation, so
use-route-resume now arms a one-shot (markSelectionRestore) before
dispatching the window's FIRST resume; the selection listener consumes
it and skips homing exactly once. Every later resume — sidebar click,
route change, reconnect — homes as before, and starting on /new
consumes boot status too so a subsequent session open still homes.
* fix(gateway): exclude current-turn media from dedup
* test(gateway): cover current-turn TTS media dedup
* fix(gateway): collect quoted/spaced/home-relative MEDIA paths into the history dedup set
Salvaged from PR #73982 by Alexander Russell (@AlexxRussell) — the
collector half only. _collect_history_media_paths used only
_TOOL_MEDIA_RE, which misses quoted and spaced paths that the delivery
pipeline's extract_media grammar accepts; run text content through the
same extractor so the surviving dedup consumers (auto-append lane and
bare-path filter) see every path that could actually have been
delivered.
The PR's other halves (post-stream dedup snapshot plumbing, canonical
path comparison in _deliver_media_from_response, queued-followup
snapshot union) are moot after #74495 removed the post-stream history
filter entirely.
* chore: contributor mappings for FraserHum + AlexxRussell (#72542/#73982 salvage)
* feat(desktop): highlight matched letters in searchable pickers
Typing in the model picker dialog, edit-models dialog, or command
palette now marks WHY each row matched: every occurrence of the query
(per-term for the palette's AND matcher) renders as an accent-colored
semantic <mark>. cmdk's scorer exposes no match ranges, so the shared
HighlightMatches primitive mirrors each surface's own filter semantics
instead: literal substring for the pickers, split-on-whitespace terms
with merged overlapping ranges for the palette.
* fix(desktop): make the composer model dropdown's search commit honestly
Two fixes to the pill dropdown's filter, following the consensus across
VS Code, Zed, Open WebUI, and Cherry Studio:
- The pinned current model no longer rides along on a query it doesn't
match. It sat above the real matches looking like the top result, so
typing 'grok' and committing landed you back on the current model.
- Enter in the search field commits the first visible match (VS Code's
'so Enter works without pressing DownArrow first'). Radix highlights
nothing until an arrow key, so Enter used to dead-end; now
open → type → Enter is the whole switch.
Matched letters also render through HighlightMatches like the other
searchable pickers.
* feat(desktop): ⌘⇧M opens the model menu on the pane under the pointer
composer.modelPicker shipped unbound and opened the full-screen picker
dialog. It now defaults to ⌘⇧M — the chord LibreChat, Open WebUI, and
Cherry Studio independently converged on — and toggles the composer
pill's live dropdown instead, search field ready: ⌘⇧M → 'gr' → Enter
switches model in two keypresses.
Routing follows the tab-verb convention (#74447): the request lands on
the chat surface in the hovered zone first, then the active composer,
skipping hidden keep-alive tabs. With no chat surface on screen
(settings, profiles) the keybind falls back to the full dialog; with
the gateway closed the pill opens the dialog like a click would.
* fmt(js): `npm run fix` on merge (#74544)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
* feat(mcp): add Comfy Cloud to the MCP catalog (remote HTTP + native OAuth 2.1)
New catalog entry for Comfy Cloud's hosted remote MCP server at
https://cloud.comfy.org/mcp — Streamable HTTP with native MCP OAuth 2.1
(Dynamic Client Registration + PKCE), the same shape as the linear entry.
Nothing to install locally; Hermes's MCP client handles discovery and the
browser flow on first connect.
The server exposes ~30 tools for AI generation on Comfy Cloud: image /
video / audio / 3D via ComfyUI workflows (submit_workflow), curated
templates (run_template), and partner models like Flux, Kling, and Veo
(partner_generate), plus job lifecycle and discovery tools.
tools.default_enabled is left unset so the install-time checklist starts
all-on, mirroring the linear entry.
Also adds the per-entry data-files target in pyproject.toml per the
one-target-per-entry pattern documented there.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(mcp): curate comfy-cloud default tool set + drop legacy packaging line
- tools.default_enabled: 20-tool curated subset (discovery, generation,
job lifecycle, billing). The server exposes ~37 tools; all-enabled adds
~16-22k tokens of schema to every API call — larger than the entire
Hermes core toolset (~12.7k). Curated default lands at ~9-12k. Batch,
saved/shared workflow, and App Mode tools remain opt-in via
'hermes mcp configure comfy-cloud'.
- report_session_summary excluded from defaults per telemetry policy
(no outbound telemetry without explicit user opt-in).
- description trimmed to catalog guideline length.
- revert pyproject data-files line: the per-entry packaging enforcement
was removed (no-pip policy); blender/unreal-engine entries have no
data-files lines either.
* chore: contributor mapping for mattmiller@comfy.org (@mattmillerai)
* Revert "remove Vercel AI Gateway and Vercel Sandbox (#33067)"
This reverts commit febc4cfec0a79b175a430304765473c97e10622f.
* modernize re-added Vercel integrations: SDK 0.7.2, telemetry off, sibling-site wiring
- Bump vercel SDK pin 0.5.7 -> 0.7.2 (pyproject, lazy_deps) and regenerate uv.lock
- Disable the SDK's new default-on telemetry (VERCEL_TELEMETRY_DISABLED=1
set before import, user-overridable) per the no-opt-out-telemetry policy
- Move _model_flow_ai_gateway into hermes_cli/model_setup_flows.py (god-file
decomposition landed after the removal)
- Widen post-removal backend sets that vercel_sandbox missed: terminal_tool
container_backend + _CONTAINER_BACKENDS, file_tools fallback set,
env_probe._REMOTE_BACKENDS, approval._should_skip_container_guards,
prompt_builder probe container_config
- Add terminal.vercel_runtime to config_defaults + TERMINAL_CONFIG_ENV_MAP
- Re-add vercel dependency group to nix #full variant (reverts #33773 workaround)
- Update restored tests to current contracts: upload-only credential sync-back
(bcfc7458fa), registry-derived provider env list, parametrized backend fixture,
drop tests superseded on main (slack wizard move #41112, nous status format)
* test: fix restored-test regressions vs current main
- test_terminal_requirements.py: restore missing 'import pytest' (revert
resurrected a parametrized test into a file whose pytest import was
pruned on main)
- test_container_cwd_sanitize.py: _CONTAINER_BACKENDS pin now includes
vercel_sandbox
* test(cli): fix order-dependent test_resume_quiet_stderr flake at the source
test_session_not_found_goes_to_stdout_in_full_mode passes in isolation but
fails in a full tests/cli run. Two independent leaks from the same neighbor
test conspire:
1. test_cli_init.py's _make_cli() reloads cli.py while prompt_toolkit is
stubbed with MagicMocks and never reloads it back, so sys.modules['cli']
is left with a mock _pt_print/_PT_ANSI and cli._cprint silently no-ops
for every later test. Fixed by reloading cli once more with the real
modules visible (try/finally).
2. prompt_toolkit's print_formatted_text caches its Output on the
process-global default AppSession the first time it renders without an
explicit output=. Under capsys (which swaps sys.stdout per test), the
first CLI test to emit through _cprint locks that cache onto its own
captured stdout, so later capsys tests read an empty buffer. Fixed with
an autouse fixture in a new tests/cli/conftest.py that resets the cached
output around each test.
Neither change touches production code or the flaky test's own assertion.
Related to #59358 (which addresses the same flaky test by mocking _cprint in
the assertion instead; this fixes the two underlying leaks at the source and
does not modify test_resume_quiet_stderr.py).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(tests): stop the suite writing into the operator's real Hermes logs
hermes_cli/main.py calls setup_logging() at module scope. That resolves
get_hermes_home() and attaches rotating file handlers to the ROOT logger via
a QueueHandler. So merely importing it - which many test modules do, directly
or transitively - points the whole pytest session's logging at
<HERMES_HOME>/logs/agent.log and errors.log.
The _isolate_env fixture already sandboxes HERMES_HOME, but fixtures run
after collection has imported the test modules, and by then the handler holds
an absolute path to the real file. Verified by importing hermes_cli.main in a
clean interpreter and walking the queue listener: both handlers pointed at the
developer's own ~/.hermes/logs/.
Measured on a live install: 126 warnings in a personal agent.log came from
test runs rather than the running gateway - phantom 'FakeTree' Discord
registration failures and 'rejected invalid API key' entries whose paths only
exist in tests/gateway/test_api_server_runs.py. That noise makes genuine
warnings hard to find exactly when someone is debugging.
conftest is imported before any test module, so sandboxing HERMES_HOME there
closes the window. The per-test fixture still applies afterwards.
Also fixes 4 pre-existing failures: tests/gateway/test_channel_directory.py
TestBuildFromSessions was reading the operator's real sessions data for the
same reason.
Full gateway+tools suites: 66 failures on clean origin/main, 62 with this
change, 0 new. The regression guard asserts the value captured AT conftest
import - reading os.environ inside a test passes even with the fix removed,
because the per-test fixture has sandboxed it by then.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(tests): tolerate mid-import kanban_db in the write-guard sys.modules probe
The guard's sys.modules.get() can observe hermes_cli.kanban_db while a
lazy import is still executing on a fixture boundary — the partially
initialized module has no .connect yet (AttributeError flake in the
full-suite verification run, 1/2460 files). A half-imported module has
no callers to guard; skip this round and let the next fixture patch the
completed module.
* feat(desktop): full cmdk-style keyboard selection in the model dropdown
Enter-commits-first was still filter-only past the first row: arrows
handed focus to Radix's own item focus, hover fought the keyboard for
which row Enter meant, and unfiltered MoA presets sat below zero model
matches as phantom commits.
The search input now owns the whole keyboard interaction over one flat
row list that mirrors exactly what's rendered (collapse, filter,
presets included — presets filter by query now too):
- no query → selection sits on the current model, Enter just closes
- typing → first match auto-selected, ↑/↓ step with wraparound (reset
on every keystroke), Enter commits the highlighted row
- selection scrolls into view; the highlight yields while the pointer
is in the list so hover and keyboard never disagree about Enter
* fix(tests): preserve config module identity in backup tests
(cherry picked from commit 1ac105ea50db6fac4a18b606a6cd9b06d7f5925d)
* fix(update): test runs never mutate the live checkout — pytest-guard the marker and repair paths
Guard the .lazy-refresh-incomplete marker writer (update_cmd), launch-time
recovery (main.py), and _early_recovery repair paths behind a two-condition
check: running under pytest AND the target is this live checkout. Sandboxed
tmp_path tests still exercise the real code paths.
Salvaged from PR #72002 by @fcavalcantirj. Fixes #72000.
Co-authored-by: fcavalcantirj <felipe.cavalcanti.rj@gmail.com>
* test: prove concurrency with barriers/witnesses instead of wall-clock bounds
Replace OS-scheduler-dependent elapsed-time ceilings with deterministic
concurrency proofs in three tests:
- test_context_refs_concurrent: asyncio.Barrier rendezvous — all 3 URL
fetches must be in flight simultaneously before any returns.
- test_memory_boundary_commit: positive non-blocking witness — the
provider call list must still be empty when the async commit returns.
- test_mem0_v3 slow-prefetch: threading.Event park/release — prefetch
must return while the backend search is still parked.
The tests/tools/test_mcp_tool.py hunk from the original PR is dropped:
main no longer carries the 'elapsed < 2.5' assertion it targeted
(superseded by a delay-relative bound).
Salvaged from #71913.
* test(tui_gateway): make the tui gateway suite order-independent
Cross-file leaks made tests/tui_gateway + tests/test_tui_gateway_server.py
fail when run in one process (issue #57068):
- conftest: _hermetic_environment now re-pins hermes_state.DEFAULT_DB_PATH
to the fake home so no test ever touches the developer's real state.db
- conftest: new autouse _reset_tui_gateway_server_state snapshots/restores
_methods, cfg cache, db handle and _real_stdout, and tears down leftover
sessions via the production _close_session_by_id(..., end_reason=
"test_cleanup") boundary
- per-file fixtures scope patch.dict(sys.modules) to the import only
- drop reload()-based teardowns that duplicated atexit hooks
Conflict resolution vs main: kept main's mod._live_transports.clear() in
the test_protocol.py server fixture alongside the snapshot/restore logic.
Combined run now 792 passed / 0 failed.
Salvaged from PR #57066 by @lEWFkRAD. Fixes #57068.
* fix(tests): register no_isolate and ssh pytest markers
Registers the two genuinely-unregistered markers (no_isolate, ssh) in
pyproject.toml, silencing PytestUnknownMarkWarning for tests/tools/test_mcp_*
and tests/tools/test_file_sync_perf.py.
Dropped hunk: the live_system_guard_bypass line from the original PR — that
marker is already registered dynamically via pytest_configure/addinivalue_line
in tests/conftest.py, so the ini entry would be a duplicate.
Salvaged from #71403.
* test(fal): pin fal_common behavioral contracts
Contract tests for tools/fal_common.py: queue-URL normalization
(trailing slash / whitespace / empty-raises), _extract_http_status
response-vs-exc precedence and non-int rejection, the
_ManagedFalSyncClient RuntimeError guards against fal_client private
API drift, and submit()'s queue-URL + POST/json/timeout wiring.
Trimmed on landing: test_non_string_coerced_to_string (pins an
implementation accident, not a contract) per the coverage-padding
policy in AGENTS.md.
Salvaged from #52166.
* fix(tests): live-system guard treats only argv[0] as the executable
Argv-list commands now scan only argv[0] against _PROCESS_KILLERS, ending
false positives like ['cat', '.../skill'] ('skill' is a real util-linux
binary name). Wrapper executables (sh/bash/env/nohup/timeout/sudo/xargs/…)
keep full-token scanning so ['bash', '-c', 'pkill …'] and
['env', …, 'pkill', …] stay blocked; string commands are unchanged.
Adds tests/test_live_system_guard.py pinning both directions.
Salvaged from PR #43299 by @eazye19.
Co-authored-by: Tony (eazye19) <support@captureclient.net>
* test(tui_gateway): pin goal-command config home against collection-time _hermes_home freeze
Sibling files (test_billing_rpc etc.) import tui_gateway.server at
collection time, freezing module-level _hermes_home = get_hermes_home()
(server.py:54) to the developer's real home before conftest isolation
runs. _load_cfg() then reads the REAL ~/.hermes/config.yaml — e.g. a
local MoA preset — instead of what _write_moa_config wrote.
The server fixture now monkeypatches _hermes_home to the isolated
HERMES_HOME and resets the mtime-keyed cfg cache (_cfg_cache/_cfg_mtime/
_cfg_path); monkeypatch restores originals on teardown.
Complementary to the #57066 autouse teardown, which only restores the
cfg cache to its pre-test value and never re-points _hermes_home.
Combined tests/tui_gateway + tests/test_tui_gateway_server.py:
792 passed.
Salvaged from PR #63981 by @lEWFkRAD.
* fix(test-runner): native Windows venv probe + glyph-safe stdio
Two W…
leewenjie
pushed a commit
to leewenjie/hermes-agent
that referenced
this pull request
Aug 7, 2026
…rop-stale-vercel-group fix(nix): drop stale "vercel" group from #full variant
randlee
pushed a commit
to randlee/hermes-agent
that referenced
this pull request
Aug 11, 2026
…ling-site wiring - Bump vercel SDK pin 0.5.7 -> 0.7.2 (pyproject, lazy_deps) and regenerate uv.lock - Disable the SDK's new default-on telemetry (VERCEL_TELEMETRY_DISABLED=1 set before import, user-overridable) per the no-opt-out-telemetry policy - Move _model_flow_ai_gateway into hermes_cli/model_setup_flows.py (god-file decomposition landed after the removal) - Widen post-removal backend sets that vercel_sandbox missed: terminal_tool container_backend + _CONTAINER_BACKENDS, file_tools fallback set, env_probe._REMOTE_BACKENDS, approval._should_skip_container_guards, prompt_builder probe container_config - Add terminal.vercel_runtime to config_defaults + TERMINAL_CONFIG_ENV_MAP - Re-add vercel dependency group to nix #full variant (reverts NousResearch#33773 workaround) - Update restored tests to current contracts: upload-only credential sync-back (60ca8f0), registry-derived provider env list, parametrized backend fixture, drop tests superseded on main (slack wizard move NousResearch#41112, nous status format)
33hodl
pushed a commit
to 33hodl/hermes-agent
that referenced
this pull request
Aug 12, 2026
…ling-site wiring - Bump vercel SDK pin 0.5.7 -> 0.7.2 (pyproject, lazy_deps) and regenerate uv.lock - Disable the SDK's new default-on telemetry (VERCEL_TELEMETRY_DISABLED=1 set before import, user-overridable) per the no-opt-out-telemetry policy - Move _model_flow_ai_gateway into hermes_cli/model_setup_flows.py (god-file decomposition landed after the removal) - Widen post-removal backend sets that vercel_sandbox missed: terminal_tool container_backend + _CONTAINER_BACKENDS, file_tools fallback set, env_probe._REMOTE_BACKENDS, approval._should_skip_container_guards, prompt_builder probe container_config - Add terminal.vercel_runtime to config_defaults + TERMINAL_CONFIG_ENV_MAP - Re-add vercel dependency group to nix #full variant (reverts NousResearch#33773 workaround) - Update restored tests to current contracts: upload-only credential sync-back (bcfc745), registry-derived provider env list, parametrized backend fixture, drop tests superseded on main (slack wizard move NousResearch#41112, nous status format)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The
verceloptional-dependency was removed from pyproject.toml in #33067, butnix/packages.nix(added a few hours later in #33108) still references"vercel"in the#fullvariant'sextraDependencyGroups. uv2nix fails evaluation with:error: Extra/group name 'vercel' does not match either extra or
dependency group
Because
nix/devShell.nixdoesinputsFrom = builtins.attrValues self'.packages, the broken#fullderivation is pulled into the dev shell too, sonix develop/ direnv breaks on a fresh clone — not justnix build .#full.What does this PR do?
Related Issue
Fixes #
Type of Change
Changes Made
How to Test
Checklist
Code
fix(scope):,feat(scope):, etc.)pytest tests/ -qand all tests passDocumentation & Housekeeping
docs/, docstrings) — or N/Acli-config.yaml.exampleif I added/changed config keys — or N/ACONTRIBUTING.mdorAGENTS.mdif I changed architecture or workflows — or N/AFor New Skills
hermes --toolsets skills -q "Use the X skill to do Y"Screenshots / Logs