hermes-agent is public/OSS; the forensic-logging comment in
_quarantine_nous_oauth_state named 'Fly' (the specific managed-hosting compute
provider) twice. Reword generically ('a hosted agent', 'a managed log drain may
be WARNING-only') — the behaviour is unchanged, only the comment. Follows the
same scrub applied to the boot re-seed helper (#59983) before merge; this one
slipped through in #59976.
When the bot PR auto-merges, main moves — which the poll loop detects
as 'main moved' and tries to close the PR. But the PR is already
merged, so gh pr close fails with an error.
Fix: when main moves, re-check PR state before closing. If it's
already MERGED, exit cleanly. Also make gh pr close non-fatal (|| true)
as a belt-and-suspenders guard against the same race on the other
close paths.
The js-autofix workflow used 'gh pr create --json number --jq .number'
to capture the PR number, but 'gh pr create' doesn't support --json.
Extract the PR number from the URL that 'gh pr create' prints instead.
tests/test_desktop_mac_entitlements.py asserts about
apps/desktop/electron/*.plist — the same CI blind spot as the other
ported tests: the change classifier routes apps/ changes to the
frontend lane, so a PR touching only the plists would skip the Python
suite and the regression would go green on the PR and red on main.
Ported to tests-js/desktop-mac-entitlements.test.ts so it runs in the
correct lane. All three tests carry over: the inherit plist grants
audio-input (regression #37718), every device.* entitlement on the
main app is also inherited, and both files remain well-formed.
Parsing uses the `plist` package (pinned to ^3.1.0, the version
already present in the workspace lockfile, so no new transitive
packages) plus `@types/plist` — Node's stand-in for plistlib.
Verified: tests-js `npm run check` passes (typecheck + 9/9 tests), and
a mutation run (removing audio-input from the inherit plist) turns
both regression tests red.
The CI change classifier routes package.json / package-lock.json /
.ts/.tsx changes to the frontend lane, not the Python lane. Four
Python tests asserted about these JS-side artifacts, so a PR touching
only those files would skip the Python suite — the regression goes
green on the PR and red on main (where the classifier fails open).
Ported all four to vitest so they run in the correct CI lane:
tests/test_package_json_lazy_deps.py
→ apps/desktop/electron/package-json-lazy-deps.test.ts
(camofox is lazy, agent-browser is eager, lockfile clean)
tests/test_desktop_electron_pin.py
→ apps/desktop/electron/desktop-electron-pin.test.ts
(electron dep is exact, matches build.electronVersion, lockfile agrees)
tests/test_assistant_ui_tap_compat.py
→ apps/desktop/electron/assistant-ui-tap-compat.test.ts
(@assistant-ui cluster shares one tap version + semver helper)
tests/test_dashboard_sidecar_close_on_disconnect.py
→ web/src/lib/chat-sidebar-session-params.test.ts
(sidecar session.create opts into close_on_disconnect + profile)
The ChatSidebar test was regex-matching .tsx source text (the
source-reading anti-pattern). Extracted sidecarSessionCreateParams()
from the component's effect into an exported pure function so the
test calls real code instead of pattern-matching a string.
Verified: 8 electron tests + 76 web tests pass; both typecheck clean.
Adds a "Tests for JavaScript / npm / package.json invariants belong
in the JS suite" subsection under Testing, documenting that the CI
classifier routes package.json / lockfile / .ts/.tsx changes to the
frontend lane — so Python tests asserting about those files won't run
on a JS-only PR. Includes a table mapping artifact types to the
correct vitest workspace and run commands.
* fix(js): never format package-lock.json
prettier and eslint should never touch package-lock.json. main has a
repo rule requiring team approval when lockfiles change, so an autofix
PR touching it would hang waiting for review.
- Add .prettierignore at repo root
- Add '**/package-lock.json' to eslint shared config ignores
* fix(ci): js-autofix pushes via PR instead of direct push to main
Main now has repository rules requiring pull requests + required status
checks ("All required checks pass"), so the workflow's direct push to
main is rejected with GH013 every time eslint --fix produces changes.
Switch apply-patch to push to a dedicated bot/js-autofix branch, create
or update a PR, and enable auto-merge (squash). The PR auto-merges once
CI passes. If CI fails or main moves, the PR is auto-closed and the
branch deleted — the next run re-applies on the current state.
The two-job security split is preserved:
- generate-patch stays unprivileged (contents: read only) — it runs npm
on an ephemeral runner with zero push permissions.
- apply-patch (contents: write + pull-requests: write) still never runs
npm, never installs anything, never executes repo code — it applies
the trusted patch artifact and delivers it via PR.
The hoisted shared eslint config catches pre-existing no-useless-escape
and no-control-regex errors in apps/desktop that were only fixed in web/
in the previous commit.
- no-useless-escape: remove unnecessary \) escapes inside character classes
- no-control-regex: add eslint-disable-next-line comments for intentional
\x1b terminal escape byte matching (same pattern as web/src/lib/pty-mobile-input.ts)
apps/shared had lint:fix but not the 'fix' alias that other workspaces
have. The js-tests check job runs 'npm run fix' as a second step, so
this workspace was failing with 'Missing script: fix'.
- Fix no-useless-escape in i18n/ko.ts and i18n/uk.ts (remove backslash
escapes inside single-quoted strings)
- Add eslint-disable-next-line for no-control-regex in pty-mobile-input.ts
(terminal data legitimately contains control characters)
- Configure react-refresh/only-export-components with allowConstantExport
so context providers that export hooks don't trigger the rule
- Downgrade react-hooks v7 rules (set-state-in-effect, refs,
preserve-manual-memoization, static-components) from error to warn —
these are real concerns but the existing code uses common patterns
(data loading on mount, ref-as-instance-var) that need careful refactoring
Add a 'lint' job to the JS tests workflow that runs 'eslint --fix'
across all discovered npm workspaces (same matrix as the check job).
Fixable issues auto-correct and don't block; eslint exits non-zero
only when un-fixable errors remain.
Also fix duplicate 'needs: workspaces' in the check job.
* feat(desktop): add background-task indicator to sidebar session rows
A session with a live terminal(background=true) process but no active LLM
turn now shows a pulsing gray dot in the sidebar — distinct from the accent
pulse of an active turn and the steady amber/green of needs-input/unread.
New $backgroundRunningSessionIds computed atom joins
$backgroundStatusBySession (runtime-keyed) with $sessionStates
(runtime→stored) to produce stored session ids the sidebar row can match
against — same pattern as $attentionSessionIds and $unreadFinishedSessionIds.
Also refactors SidebarRowDot from a 3-branch if-else chain into a
table-driven priority array of DotState entries. Each state declares its
active flag, className, ariaLabel, and title in one place — adding a new
indicator state is now one array entry instead of another conditional.
* fix(desktop): keep pulse-dot before:bg-* static so Tailwind emits it
The PING() helper interpolated the color into `before:bg-${color}`, but
Tailwind v4 only generates utilities it finds as complete static strings — a
template-composed `before:bg-(--ui-accent)` / `before:bg-muted-foreground/50`
is never emitted, so both pulse rings (working + new background) lost their
halo color. Make PING a static scaffold and write the before:bg color inline
per variant.
---------
Co-authored-by: Brooklyn Nicholson <brooklyn.bb.nicholson@gmail.com>
Thinking-only retries flush the scrubber then stream again without
reset(). flush() was leaving _last_emitted_ended_newline False, so the
next stream's opening <think> looked mid-line and leaked into the UI.
SessionInfo already carries git_branch from the backend, but neither the
ctrl-k command palette nor the sidebar search function included it in
their matching fields. Typing a branch name matched nothing.
- session-search.ts: add session.git_branch to sessionMatchesSearch
- command-palette/index.tsx: thread git_branch through SessionEntry and
into the keywords array for both sessions and archived sessions groups
- session-search.test.ts: cover full, partial, and "main" branch matches
* fix(desktop): un-minimize zone when revealing a collapsed tool panel
`revealTreePane()` fronted the pane's tab but never un-minimized its zone
when the pane lived in a shared zone (terminal + logs, or a tool panel
stacked with the workspace). The shared-zone branch of `setPaneCollapsed`
routes opens through `revealTreePane` instead of `toggleTreeGroupMinimized`,
so the zone stayed collapsed and the terminal appeared to "close but not
open" on Ctrl+` — and tab clicks needed a double-click because the first
click's `restoreTreePane` → opener → listener → `revealTreePane` chain
didn't un-minimize either.
The fix: `revealTreePane` now restores a minimized zone before fronting
the pane, chaining the un-minimize + activate into a single `commit` so
the second op sees the updated tree.
* fix(desktop): side collapse in column-root layouts + restore after zone-menu minimize
Two follow-up fixes for the panel layout:
1. Ctrl+B / Ctrl+J sidebar toggles didn't work in the Terminal deck (and
Quad) layout. The side-collapse system (treeSideOfPane, paneRootSide,
layoutHasRootSide, and the renderer's semanticSides) only operated on the
ROOT split when it was a row — but Terminal deck's root is a column, so the
side columns (sessions/files) nested inside a child row were invisible to
the collapse system. Extracted a rootRow() helper that finds the row
containing main (the root itself for row layouts, or the row child of a
column root), and a rootRow prop propagated through TreeNode → TreeSplit so
semanticSides fires on the right split regardless of root orientation.
2. Clicking the "logs" tab in a minimized terminal/logs zone needed a
double-click when the zone was minimized via the zone menu (right-click →
minimize), not the toggle. restoreTreePane called the opener
($logsOpen.set(true)) and returned early — but when the store was already
true, nanostores don't fire the listener, so setPaneCollapsed/revealTreePane
never ran and the zone stayed minimized. Now restoreTreePane also
un-minimizes directly + calls revealTreePane when the zone is still
minimized after the opener runs.
Follow-up on kshitijk4poor's cherry-picked references:
- pitfalls.md rewritten from the raw-socket blender_exec() frame to the
MCP-tool frame (dropped 'MCP server is optional, talk to the socket
directly' — now the anti-pattern; dropped TCP-helper internals items;
kept all bpy/addon knowledge: empty code results in 5.x, temp-file
readback, ops-vs-data context, engine names by version, GPU setup)
- recipes.md: blender_exec -> execute_blender_code in the agent-side
verification snippet
- SKILL.md: reference-file table added to Quick Reference, version
2.1.0, kshitijk4poor added to authors
- docs page regenerated (scoped to this skill)
Fixes#62452. Two amplifiers turned one slow auxiliary route into a
per-turn multi-minute stall:
1. Fallback candidates inherited the exact effective_timeout the primary
was called with. When the primary's deadline was short (tuned or
already burned), an independently healthy fallback died on the same
clock — the reporter's 163k-token compression needed ~90s on the
fallback and got the primary's 30s, every turn. fallback_chain
entries may now declare their own 'timeout' (seconds); both fallback
candidate call sites (sync + async) resolve it via
_fallback_entry_timeout, label-scoped so only configured-chain
candidates are affected. No entry timeout → task-level timeout,
preserving existing behavior.
2. A session whose transcript structurally cannot be summarized within
the deadline re-attempted every 60s, re-burning the full timeout on
every subsequent turn. Consecutive timeout-class failures now
escalate the cooldown 60s → 300s → 900s (capped); any successful
summary or session reset clears the streak. Timeout classification
takes precedence over the streaming-closed 30s rung ('timed out'
also matches _is_connection_error) and now recognizes the SDK's
'Request timed out.' phrasing.
Fail-safe behavior is unchanged: all messages are preserved when every
candidate fails; the cooldown only spaces out retries.
The served-profiles block in _start_secondary_profile_adapters references
PairingStore, but the class's only import in gateway/run.py is method-local
inside __init__ — so the reference raised NameError at runtime, silently
swallowed by the enclosing try/except ('could not record served_profiles').
Result: multiplexing gateways never created per-profile pairing stores, and
authz pairing checks for secondary profiles fell through to the global
whitelist. Also masked the served_profiles runtime-status write.
One-line fix (local import alongside write_runtime_status) + regression
tests that drive the real method and assert the stores materialize,
verified red without the import and green with it.
Surfaced during the profile-routing sweep by @CocaKova's PR #61689, which
included the same fix as part of a larger feature.
* fix(computer-use): target Linux app windows reliably
Resolve app filters through the canonical cua-driver MCP app metadata and join running app PIDs back to windows. Preserve an exact selected window across capture_after, support direct capture by pid/window_id, and send the active window ID for coordinate pointer actions on Linux.
Co-authored-by: annguyenNous <annguyenNous@users.noreply.github.com>
Co-authored-by: grimmjoww578 <willies578@gmail.com>
Co-authored-by: ai-ag2026 <261867348+ai-ag2026@users.noreply.github.com>
* fix(computer-use): address review on PR #63725
Address three review comments from @f-trycua:
1. type_text, press_key, and hotkey now carry _active_window_id and
fail closed when it is missing. Previously they sent only the PID,
so CUA Driver fell back to the first window for that PID — input
could reach the wrong window in multi-window apps.
2. Coordinate scroll x/y are now capability-gated behind
input.scroll.coordinates. CUA Driver 0.7.1 Linux schema rejects
x/y on scroll; omitting them when the driver doesn't advertise
support avoids the schema rejection while still routing via
window_id.
3. Windows are sorted by z_index descending (higher = front, per CUA
Driver semantics) instead of ascending. Null z_index (Wayland) is
coerced to 0 in _ingest_windows so it doesn't crash the sort and
sorts to the back instead of being selected as the capture target.
---------
Co-authored-by: LeonSGP43 <cine.dreamer.one@gmail.com>
Co-authored-by: annguyenNous <annguyenNous@users.noreply.github.com>
Co-authored-by: grimmjoww578 <willies578@gmail.com>
Co-authored-by: ai-ag2026 <261867348+ai-ag2026@users.noreply.github.com>
The cross-turn stale-call circuit breaker (_check_stale_giveup in
agent/chat_completion_helpers.py) raises a RuntimeError when the
provider has been unresponsive for N consecutive stale attempts.
This error was classified as FailoverReason.unknown (retryable=True,
should_fallback=False), causing the retry loop to burn all max_retries
against the same dead provider — each retry hitting the circuit breaker
instantly with zero network overhead — before fallback was attempted.
Add a classification rule (section 7b in classify_api_error) that
recognizes the stale-breaker RuntimeError by its signature phrases and
classifies it as FailoverReason.timeout with retryable=False,
should_fallback=True. This makes the retry loop skip retries and go
straight to fallback provider activation on the first hit.
Test: test_stale_breaker_runtime_error_triggers_fallback_not_retry
The #54878 self-heal (SessionStore.get_or_create_session) drops a routing
key pointing at a session already ended in state.db and recovers/recreates
a fresh session_id under the same session_key. The #54947 fix (agent-cache
cache-hit guard in gateway/run.py) treats a cached agent whose snapshot
session_id differs from the current session_id, under the same
session_key, as an intentional /resume-/branch-style switch between two
live sibling conversations, and reuses it unchanged to protect the prompt
cache.
These two fixes compose incorrectly: when the #54878 self-heal just fired,
the cached agent's session_id is not a live sibling — it's the dead session
just routed away from. #54947's "different session_id -> reuse freely" rule
reuses it anyway. The stale agent runs the turn, and the post-run "session
split" sync (agent.session_id != session_id) then writes the routing key
straight back onto the dead session_id, undoing the self-heal. This repeats
on every subsequent message until an interrupt (e.g. /stop) happens to race
in before that post-run sync, silently discarding conversation context.
Reproduced live on the engineering gateway (2026-07-12, routing key
agent:main:telegram:dm:170829464:544520): 5 consecutive self-heal log lines
over ~40 minutes, each followed by the dead session_id being reused and
re-synced back, until an interrupted /stop finally let a fresh session
stick — at which point all prior context was gone.
No open upstream issue tracks this specific interaction as of 2026-07-12
(checked #54878, #54947, #59580, #59597, #61220 — all cover adjacent but
distinct edges of the self-heal / agent-cache system).
Fix: before applying #54947's reuse-on-mismatch rule, check (outside the
cache lock, via SessionStore._is_session_ended_in_db) whether the cached
snapshot's session_id is itself ended in state.db. If so, treat it as a
stale self-heal artifact and evict/rebuild fresh -- same as a genuine
cross-process write -- instead of reusing it. Re-validates the peeked
verdict against the tuple actually held under the lock so a race can't
apply a stale verdict to a different (possibly live) cache entry.
Tests: tests/gateway/test_stale_self_heal_agent_cache_eviction.py (5 new
cases: dead-session eviction, live-sibling reuse preserved [#54947 intact],
cross-process invalidation preserved [#45966 intact], same-session_id dead
edge case, lock-race re-validation). Full tests/gateway/ suite: 14 failed,
9040 passed, 11 skipped -- all 14 failures verified pre-existing on
unpatched main (confirmed via git stash + re-run), unrelated to this
change.
Reasoning ("Thinking") rendered through MarkdownTextContent with a smooth
typewriter reveal. TextMessagePartProvider mints a fresh part object on every
text change, and useSmooth resets its reveal to empty whenever the part
identity changes — so each reasoning delta restarted the animation from the
first character (h / hell / hello wo ...). Token-streaming reasoners
(R1/Qwen/GLM/Claude thinking) fire a delta per token and flash hard; GPT-5's
coarse reasoning summary updates too rarely to notice, which is why it looked
fine.
Drop smooth on the reasoning path so it plain-appends, exactly like the
assistant answer already does. Removes the now-dead smooth plumbing too.
GatewayRunner._prepare_inbound_message_text() interpolated
source.user_name — the platform-supplied, user-settable display name —
directly into the message text of every turn in a shared multi-user
session: f"[{source.user_name}] {message_text}". Shared sessions are the
default for any threaded conversation (thread_sessions_per_user defaults
to False) and apply to any group with group_sessions_per_user=False, so
no special configuration is needed to reach this path.
An unescaped display name containing embedded newlines could therefore
masquerade as a new markdown section (a fake "## Override" heading)
inside the live conversation the model reads on every turn — the same
indirect-prompt-injection vector gateway/session.py's
build_session_context_prompt() already guards against for the identical
user_name field via _format_untrusted_prompt_value(), which was never
applied to this sibling call site.
Add neutralize_untrusted_inline_text() alongside the existing helper in
gateway/session.py: it collapses embedded newlines/control characters to
a single inert line without JSON-quoting, so inline "[Name] message"
formatting is preserved byte-for-byte for the common case (unlike
reusing _format_untrusted_prompt_value directly, which would add visible
quote marks to every sender prefix). Wire it into the sender-prefix
construction in gateway/run.py.
Generalize the clarify opt-out into an UNBOUNDABLE_TOOLS set so a run
containing image_generate also stays a plain, fully-visible stack instead
of collapsing into the bounded window, where the max-height + gradient mask
clipped the image the same way it clipped clarify forms.
When an agent turn finishes while the user is viewing a different
session, the sidebar now shows a steady green dot on that session —
distinct from the blue pulsing dot of a running turn and the gray dot
of an idle one. Opening the session clears the indicator.
The unread state is ephemeral renderer-side state, matching the
existing $workingSessionIds and $attentionSessionIds pattern: no
persistence, no backend involvement, wiped on gateway-mode switch.
Co-authored-by: liuhao1024 <liuhao1024@users.noreply.github.com>
Co-authored-by: dschnurbusch <dschnurbusch@users.noreply.github.com>
Co-authored-by: Flow Digital Inc. <flow-digital-ny@users.noreply.github.com>
Bug 2 of #28156: the picker offered us./global. inference profiles to
EU-region endpoints (unroutable — AWS rejects them regardless of
credentials) and _RECOMMENDED hardcoded us.anthropic.* ids, so non-US
pickers pinned profiles their endpoint can't invoke.
- bedrock_model_routable_from_region(): geo-prefixed profiles are only
offered in their own geography (full AWS prefix set incl. apac./jp./
ca./sa./me./af.); bare ids and global.* pass everywhere; unknown
region shapes hide nothing.
- Recommendations match geo-agnostically on the base model id, so an EU
picker pins eu.anthropic.claude-sonnet-4-6; in-region geo profiles
sort above global.* for the same model (addresses the global.*-first
ordering complaint from the issue thread).
- Dedup generalized from (us., global.) to all profile prefixes.
Rewrites the cherry-picked test to import os locally and monkeypatch the
resolver seams instead of patching bedrock_adapter internals; adds the
inverse assertion (no bearer -> AnthropicBedrock SDK path preserved).
Three fixes for the Bedrock Claude path:
1. Streaming fallback: When AnthropicBedrock SDK raises 'Unexpected event
order' (SDK misparses Bedrock error events as message_start), auto-switch
to native Converse API for the rest of the session instead of failing
after 3 retries.
2. Image base64 decode (#33317): data URL payloads were passed as base64
strings to source.bytes, but boto3 re-encodes at the wire layer. Now
decoded to raw bytes before passing to Converse API.
3. Bearer token routing (#28156): Users with AWS_BEARER_TOKEN_BEDROCK are
now routed through Converse API regardless of model, since the
AnthropicBedrock SDK only supports SigV4 signing.
3 new tests. 121 bedrock_adapter tests passing.
The docs search theme (@easyops-cn/docusaurus-search-local) defaults
fuzzyMatchingDistance to 1, so every term also matched words one edit
away. Two user-visible failures on the 14.4 MB production index:
- Wrong results: 'keet' returned 'Microsoft Teams Meetings',
'google_meet', 'Keep the Model Loaded' etc. — 'meet' and 'keep' are
both one edit from 'keet', and the stemmer indexes 'meetings' as
'meet'.
- Search appearing to die: fuzzy matching multiplies the generated
lunr queries (distance matrix x maybe-typing variants x
leave-one-out terms — up to 210 queries per keystroke on multi-word
input), and fuzzy REQUIRED terms are the expensive scan kind. A
typo'd 3-word query stalled the single-threaded search Web Worker
for 25+ seconds; every later keystroke's search queued behind it,
so the bar stopped returning results.
Setting fuzzyMatchingDistance: 0 keeps exact-word-or-prefix semantics
(keet -> keet*), which is the behavior users asked for. Validated by
running the plugin's shipped smartQueries/tokenize code against the
downloaded production search-index.json: legitimate queries (cron,
telegram, prefix 'memor') return identical results; worst-case typo
queries drop from 210 queries / 365-532ms per keystroke to 50-105 /
53-188ms; false 'keet' matches gone.
Follow-up hardening on top of #64158 (@DavidMetcalfe):
Backend (the root-cause fix):
- hermes_cli/moa_config.py: add validate_moa_payload() — strict write-time
counterpart to the deliberately tolerant normalize_moa_config(). Flags
half-filled slots, empty reference lists, recursive moa slots, naming the
exact preset/slot.
- hermes_cli/web_server.py: PUT /api/model/moa validates before normalizing
and returns 422 with the specific problems instead of silently swapping the
user's preset for hardcoded defaults (#64156). Also declares
fanout / reference_max_tokens / reasoning_effort on the Pydantic payload so
client round-trips no longer erase hand-set values.
Desktop:
- Replace sanitize-then-send with hold-while-incomplete: the debounced
autosave is deferred (not repaired) while any slot is half-filled, and
flushes once the model pick completes the edit. Mid-edit UI state is never
repainted by a save response (generation guard covers held edits too).
- updateMoaSlot only clears the model when the provider actually changed.
- Explicit preset ops (set default / add / delete) cancel the pending
autosave and invalidate in-flight responses so the two writers can't race.
- Stable row keys (preset+index) so mid-edit rows don't remount; cleared
model shows the 'Model' placeholder instead of vanishing.
Both TS clients' MoaConfigResponse types now declare the round-tripped
fields (fanout, reference_max_tokens, reasoning_effort).
Tests: 12 new backend unit tests (validate_moa_payload contract incl.
validate/normalize agreement), 3 new web_server endpoint tests (422 on
half-filled ref/aggregator, fanout round-trip), 3 new desktop vitest cases
(autosave held while half-filled, flush on completion, same-provider
reselect no-op). E2E validated against a live TestClient with isolated
HERMES_HOME: bug sequence now 422s with config untouched.
Fixes#64156