chore: sync Hermes upstream - #9
Merged
Merged
Conversation
AlertTitle clamps to one line, so Desktop error toasts hide the rest of the message behind an ellipsis. Override that clamp, let the title wrap, and cap height so a huge error scrolls instead of covering the chat.
Cover the wrap override and height cap so a one-line clamp cannot hide the rest of an error toast again.
…ostores The Desktop renderer crashes with `RangeError: Invalid array length` thrown from `Array.push` inside nanostores' `notify()`. The shared `listenerQueue` grows without bound because `reconcile()` is subscribed to BOTH `$sessions` and `$pinnedSessionIds`, and `pullRemotePins()` mutates `$pinnedSessionIds` (via `pinSession`/`unpinSession`), which fires `reconcile()` again synchronously. The existing `mirrored`/`pending`/`unconfirmed` fences only cover a *bounded* single-toggle echo. They do not cover the *unbounded* oscillation that occurs when two profiles share a session id with conflicting `pinned` flags (copied or imported profile databases). A profile-blind pull then pins and unpins the same durable id in one pass, re-firing `reconcile` forever until the queue overflows and the renderer dies. Two changes: 1. `rowsByPinId()` collapses the cross-profile session list to one authoritative row per durable pin id, preferring the active gateway's profile (the same tie-break `resolveLoadedRow` uses). `pullRemotePins()` iterates the deduped rows, so a conflicting duplicate can no longer pin then unpin the same id in a single pass. 2. A re-entrancy guard on `reconcile()` so a synchronous re-entry (from the `$pinnedSessionIds` listener firing during `pullRemotePins`) returns immediately instead of recursing. Also fixes a latent TDZ `ReferenceError` in `session-unread.ts`: `isPlainRecord` was declared after its first use through `persistentAtom`, so decoding a persisted value could throw `Cannot access 'isPlainRecord' before initialization`. Regression tests cover the duplicate-id oscillation and the active-profile tie-break.
Unscoped getSession hits the primary backend. A 404 then skipped the active profile in the remaining probes, so chats on a non-default profile never loaded. Co-authored-by: Michael McAllister <michael@empowerlo.com>
2,248 lines of gateway REST client become twelve modules by domain, with hermes.ts left as a barrel so all 144 importers stay put. The import graph is a star — every domain module imports only ./client, and client imports nothing back — so there are no cycles. The barrel names client's public exports rather than re-exporting it wholesale. Splitting a module forces its private helpers into exports so siblings can reach them, and export * would then republish them: profileScoped, connectionScoped and capabilityScoped were private to hermes.ts and have to stay that way, or a call site can assemble its own request scope and drift from the api layer.
Glass was macOS-only because it rode setVibrancy. Windows 11 22H2 has a first-party equivalent in setBackgroundMaterial, so the mode now resolves its backing per platform instead of per-OS-check: macOS keeps vibrancy, Windows 11 gets DWM acrylic / tabbed / mica, and everything older stays on Clear. No third-party native addon. Two Windows-specific details the mapping has to respect. DWM only paints the client area of a transparent window (electron#49443), so glass-capable Windows chat windows are born transparent with the opaque themed backgroundColor covering them while glass is off — a live Clear/Glass toggle then needs no window recreate. And Windows exposes three backdrops for four frost rungs, so the two heaviest both resolve to mica; the mapping stays total so a frost saved on a Mac still renders. Glass support is computed once from os.release() and shared: main uses it for the persisted default and every window, preload publishes it to the renderer so the UI can't offer a mode the window can't back.
Grouping → Profile persists ALL even with a single profile. Recents filtered that pool against the __all__ sentinel and emptied the list. Cron and messaging already used filterSessionsByProfileScope; recents now does too. Co-authored-by: andyst-dev <150129844+andyst-dev@users.noreply.github.com>
…ules The monolithic if/else-if dispatcher becomes nine modules by event family. The routing preamble runs once, then each handler consumes its own types and reports whether it did, so dispatch stops at the first taker. Families are mutually exclusive by type, so ordering between them is inert; ordering within a family is unchanged. Restores two things the extraction dropped against a moving base: the layout.apply handler, and the multi-question clarify.request path. A batch clarify was consumed and never parked, so the agent blocked on clarify.respond with no card rendered — the existing tests passed because they assert "exactly one clarify card", which is also true when the request is dropped and only the tool.start row exists.
Types, part builders, tool parts, hydration and reconciliation, behind a barrel that keeps the @/lib/chat-messages path. The folder was added without removing chat-messages.ts, so resolution preferred the file and all its importers kept hitting the monolith while the new modules sat dead. Deleting it surfaced a missing preset field on GatewayEventPayload that layout.apply needs, hidden until the folder actually resolved, and a completeOpenStreamParts helper copied into two modules when only one calls it.
52 handlers move into five registrars — git, pet overlay, hud, fs and terminal. Each takes injected deps (window handles, binary resolvers, path hardening) following the existing electron/ module pattern rather than closing over main.ts locals, and terminal-ipc returns its dispose helpers so SSH teardown and app shutdown keep working.
The row offered one unlabelled 0-100 slider whose meaning changed with the mode. Under Clear it is window opacity; under Glass it was never opacity at all — it sets how much of the theme tint stays painted over the material. Same track, same percent readout, two different things. Glass now gets a labelled panel: Tint keeps the renderer lever, Fade is a real native opacity on the ramp Clear uses, defaulting to 0 because fading a glass window fades its text — the thing Glass exists to avoid. Frost offers only the rungs the OS renders distinctly, so Windows shows three instead of two buttons that composite identically; a frost saved on a Mac highlights the button that renders the same backdrop rather than leaving the picker blank, and is not rewritten. Linux loses the row entirely, from the page and from settings search. setOpacity is a documented no-op there and there is no material, so both halves were dead — a lever that moved a number and changed nothing.
Splitting the god files made a pile of copy-paste helpers visible and, for the first time, fixable — sharing them previously meant importing a god file. Hashing function bodies through the TypeScript AST found twelve groups desktop-wide; production code is now at zero duplicates. Each helper went to the module that already owns its concern: firstStringField to lib/text, the two REST 404 predicates to lib/gateway-rpc beside isMissingRpcMethod, useDebounced and prefersReducedMotion to their hooks, the superseded-bootstrap guard to electron/ssh-connection, the composer keyup handler to the trigger hook that owns the rest of that state machine, and clampDataUrlReadMaxMb to apps/shared, replacing a "keep these in sync" comment between two copies. Only helpers with no existing owner got a new file: lib/mcp-servers, lib/audio-context, lib/keyed-timeouts, lib/pointer-drag, and the command palette's status row. Error-shape predicates are the worst thing to copy — when the backend changes how it reports a missing route, every copy has to be found.
Deciding whether the OS can back glass needs os.release(), but every Hermes window runs its preload with sandbox: true, where require is a polyfill limited to electron, events, timers and url. The node:os import threw before contextBridge ran, so window.hermesDesktop was never defined and the app booted straight into "Desktop IPC bridge is unavailable". Main already computes both verdicts, so preload asks for them over a synchronous channel instead. No reply degrades to no glass, which is an ordinary opaque window rather than a page thinned over nothing.
Comments naming gateway-event.ts, chat-messages.ts and hermes.ts as the place to look, for files those symbols no longer live in.
Perfectionist wants values before types in the glass/Windows barrel.
The same AST sweep over specs found fixtures maintained in parallel across suites that have no reason to know about each other. Twenty-one specs each mounted useMessageStream themselves and ten of the harnesses were byte-identical; twenty now take renderMessageStream, with overrides for the seams that genuinely vary. The SessionInfo builder was spelled out field-by-field in seven specs, so a new backend field broke seven files instead of one. Twenty specs carried their own inert ResizeObserver and eleven repeated the animation-frame, CSS.escape, scrollTo and WAAPI stubs the transcript needs to mount at all — split by scope into src/test/jsdom for what any component might need and the assistant-ui folder's own kit for the transcript. Plus the window-state bridge, deferred, the external-store thread runtime, the manual createRoot harness, and the per-folder caret, env-var, provider and session fixtures. Left alone on purpose: the store suites' makePrimary, where the vi.mock harness around it is the actual duplication and cannot be hoisted out of a hoisted factory; electron's deferred, where reaching into src/ from the main process would invert the layering for eight lines; and the two suites that compose another hook alongside the stream.
…ion-lookup fix(desktop): scope session lookup to the active profile (supersedes NousResearch#79522)
…l-profiles-scope fix(desktop): keep recents when ALL-profiles scope has one profile (supersedes NousResearch#84313)
feat(desktop): back window glass with Windows 11 materials, and scope its controls per OS
Review "Ask Hermes to open PR" was a window-level event that every mounted composer claimed with `target === 'main'`, so one click shipped every open session and project with dirty files. Bind the request to the visible surface captured at click time. Co-authored-by: unsupportedpastels <theoldwizard123@pm.me> Co-authored-by: youtiaowei <youtiaowei@users.noreply.github.com>
The ship button always targeted `main`, so a tile Review still prompted the workspace session. Remember the originating composer target with the pane's cwd, capture the live surface at click, and toast if that chat isn't on screen instead of dropping the click. Co-authored-by: unsupportedpastels <theoldwizard123@pm.me> Co-authored-by: youtiaowei <youtiaowei@users.noreply.github.com>
…to dim The placeholder hint and its synthetic cursor chip hand-rolled truecolor escapes ([38;2;r;g;b / [48;2;r;g;b]) and wrote them raw past Ink's depth layer. Legacy Terminal.app has no truecolor parser — it walks compound params one by one, so the literal 2 in 38;2;… lands as SGR 2: dim ON, with no 22m ever emitted. Every frame that painted the placeholder left the terminal's dim attribute stuck, and subsequent cells rendered dimmed until an unrelated bold span's 22m happened to clear it — text randomly flipping dim and back, worst right after the composer empties. Measured on a live resumed session (PTY capture, params interpreted the legacy way): 1026 glyphs painted with stuck dim on main, 0 with the fix. Route both helpers through Ink's own colorize, the same repair colorizeEcho got for the fast-echo path (gray-accent bug) — the escape now downgrades with the terminal's real color depth, and a 256-color terminal gets 38;5;N it can actually parse. Also harden hermes-ink's transitionAnsiCodes for compound SGRs: real tool output ships [1;31m-style sequences whose endCode is [0m, dodging the endCode-based weight detection — parse the params instead (skipping 38/48 extended-color arguments) so a compound bold→dim transition passes through SGR 22 too.
eslint no-control-regex rejects the CSI regex even though ESC is the sequence we have to parse.
…dfiles refactor(desktop): decompose god files into atomic modules
…mit-isolation fix(desktop): isolate review submit to one composer (supersedes NousResearch#90097, NousResearch#85911)
The renderer's `tour.request` handler ships in the desktop bundle, but the tool is offered by the backend, and the two update on different clocks. A desktop build older than the tour tool receives the event in a renderer with no branch for it, so `tour.respond` never comes and the agent blocks for the full 45s deadline — once per action the model tries. A single "give me a tour" turn (targets, then narrate, then stop) stacked those waits into minutes of dead air, which is what got reported against NousResearch#89620. Hold a session's first action to a deadline a working renderer cannot miss, and let an unanswered probe mark the bridge unavailable for that session: later calls return immediately with an error naming the actual fix instead of stalling again. Once a client has answered, real actions get the full deadline back, so a preview tour injecting into a live page still works and one slow action no longer condemns a live client. The verdict lives on the session record, so it dies with the session and a new one re-probes. The same five-action sequence goes from ~225s of dead air to a single 10s probe. Toolset gating is unchanged: removing the tool outright needs a client capability declared at session.create, which prompt caching means can only take effect for a new session.
Update the sibling tests that pinned the old use_gateway-writing
contract: image/video selector and reconfigure rows now assert the
single provider string ('nous' managed / 'fal' BYOK) plus legacy-key
popping, the stt/video picker writes drop the use_gateway expectation,
the web_server managed-browser select asserts the persisted 'nous'
cloud_provider, and explicit-local STT pins no-cloud-fallback against
a stored raw-config selection.
… picker normalize The SDK now returns the registry rows per its documented contract (salvaged NousResearch#89893), while desktops predating the SDK unwrap resolve the raw registry envelope. The plugin normalize accepts both, so the picker works across the transition; regression test updated to pin the dual-shape normalize.
…ls (config model.execution_guidance) Un-fences OPENAI_MODEL_EXECUTION_GUIDANCE from the gpt/codex/grok substring check and gives it its own injection gate, independent of tool_use_enforcement, controlled by config.yaml `agent.execution_guidance` (auto/true/false/list — same semantics as tool_use_enforcement). The "auto" list (EXECUTION_GUIDANCE_MODELS) now also covers deepseek, kimi, qwen, glm, minimax, mimo, and mistral. Composio agentic-eval traces showed Hermes+DeepSeek/Kimi failing where competitors passed: financial math done in prose, no read-back after external writes, malformed identifiers "repaired", completeness claimed despite count mismatches. The discipline block existed but those models never received it. The block is extended with compact clauses distilled from that analysis: - external-write read-back (tool-call success is not task success; internal file edits already confirmed by the tool are not re-verified) - count reconciliation (declared totals/has_more are hard assertions) - literal preservation (never normalize identifiers that fail a stated format; lookup success does not validate a malformed token) - retry-differently (empty/partial/suspiciously narrow results get a broader retry before concluding) - completion gated on verification (done = every named acceptance criterion verified, never a plausible subset) The todo tool description now encourages enumeration-as-checklist for "all N items" tasks and gates completed status on verified work, never intent. Guidance is chosen once at session start keyed on model name, so the system prompt stays byte-stable for the life of a conversation. Supersedes/absorbs prior contributor proposals: NousResearch#20588, NousResearch#35087, NousResearch#41874 (MiMo), NousResearch#53847 (GLM tool-calls-as-text stall). Co-authored-by: Mat-London <56627804+Mat-London@users.noreply.github.com> Co-authored-by: intelac <8803887+intelac@users.noreply.github.com> Co-authored-by: 6ylqq <51219463+6ylqq@users.noreply.github.com> Co-authored-by: tauros1983 <267660491+tauros1983@users.noreply.github.com>
Composio-style MCP servers return un-paginated 22-47K-char payloads that
sail under the generic 100K per-result spillover threshold, bloating
context and ballooning per-turn reasoning time on long conversations.
Competitors cap harder (OpenCode/pi 50KB, Claude Code 30K, Codex ~10K
tokens). Three changes:
- mcp_* tools spill at a tighter 50K default (BudgetConfig.mcp_result_size,
config-overridable via tool_budget.mcp_result_size_chars; pinned and
per-tool overrides still win; capped by the context-scaled default).
- The persisted-output preview now teaches recovery: page the saved file
with read_file or process with execute_code instead of re-requesting the
same data from the remote API.
- Untrusted/MCP string results are scanned (bounded, first 64KB) for
provider-side elision markers ('...N more items', "has_more": true,
'saved to sandbox', data_preview) and get ONE cache-safe incompleteness
notice appended at result-construction time, before untrusted wrapping —
so the model stops treating provider-elided enumerations as complete.
- Hard 2M-char allocation cap in mcp_tool.py (text, error, and
structuredContent paths) so a pathological multi-MB server payload is
bounded before it propagates, while ordinary large results reach
spillover intact. Distilled from NousResearch#56060/NousResearch#56072/NousResearch#56511 (issue NousResearch#56059);
supersedes their 50K lossy truncation with spillover-friendly semantics.
Docs: configuration.md spillover-budget section + cli-config.yaml.example.
Co-authored-by: Stoltemberg <215755014+Stoltemberg@users.noreply.github.com>
Co-authored-by: AlexFucuson9 <295703459+AlexFucuson9@users.noreply.github.com>
Co-authored-by: Tranquil-Flow <66773372+Tranquil-Flow@users.noreply.github.com>
The persistent terminal is a position:fixed overlay that chases its slot's rect, and the whole tracker — visibility included — was gated behind the renderer pause. Switching tabs while the window is unfocused therefore left the overlay parked over the zone at full opacity with pointerEvents:auto, so the chat underneath was unreachable until something refocused the window. Visibility is correctness rather than perf, so sample it on every wake even while paused; the rect chase, which is the part that forces layout, stays gated.
…caled stale timeouts (agent.run_budget_seconds / --run-budget)
…-intent recovery (agent.stall_guards)
Composio eval traces showed Hermes wasting turns re-issuing identical tool
calls (same tool, same args, same result — 3x/4x in one run) and ending
turns by announcing an action it never took. Two conservative, config-gated
guards (agent.stall_guards, default true):
- Identical-call loop breaker: ToolCallGuardrailController.observe_identical_call
tracks the consecutive streak of (tool, canonical args, result-hash); on
the 3rd identical call a compact one-line notice is appended to that tool
RESULT at construction time (cache-safe — tool results are append-only).
Never blocks the call. Pollers (process, *_get_result, *_poll) are exempt
via STALL_GUARD_REPEATABLE_TOOLS. Streak resets on any different call,
changed result, or new turn. Observed on the raw result before the
tool-loop warning suffix so its changing count can't defeat matching.
- Said-continue-but-stopped recovery: trailing_continue_intent() detects a
short reply ENDING on an announced next action ('Let me now…', 'I will
now…', 'Next, I…'); the conversation loop feeds it into the EXISTING
intent-ack continuation path (same interim-assistant + user-nudge
mechanism, same codex_ack_continuations cap of 2), preserving message
alternation — no parallel recovery machinery.
Config: agent.stall_guards in DEFAULT_CONFIG; docs in configuration.md;
unit tests for streak/allowlist/reset/gate and detector pos/neg cases.
…ab-trap fix(desktop): terminal pane no longer traps the window when you switch tabs
…p uses The HUD asked for vibrancy directly and always with the 'hud' material — one of the two rungs the macOS census rejected, because it collapses into under-window on blur and so changed the frost the moment another app took focus. It also ignored the translucency setting entirely: Glass off still frosted, and Windows got nothing at all. hudFrostFor is the mapping for a transparent window, beside vibrancyFor in the shared module both processes read. Two gates give it its answer: the renderer's report that the band actually covers the window, and the user's Glass setting. Off resolves to no material rather than a resting one, since a transparent window has no opaque page to hide an unwanted frost behind. Windows 11 rides setBackgroundMaterial through the same call, so the HUD follows the frost ladder on both platforms. Main self-diffs and keys the latch to the window, so a Settings change re-frosts a live HUD, a tint drag touches nothing native, and a HUD respawned on another profile is not mistaken for the window that already carried the material.
…lass The band wore its own card tint at a hardcoded 80/92%, so a HUD beside the docked window read as a lookalike rather than the same surface, and the Tint slider moved one and not the other. It now paints --ui-bg-chrome at --translucency-glass-keep: one painter, one token, one lever. That needed the setting and the surface rewrite to stop being one flag. data-hermes-glass means "this window's field surfaces may be rewritten" and is deliberately false in the HUD, which owns its own backgrounds; the new data-hermes-glass-on means "the user's Glass setting is live" and is published everywhere, along with the tint number the band reads. The 0.5rem side inset drops to zero while glass is on. It exists to keep an opaque sheet clear of the bar's corner controls, but the frost is the whole window — an inset sheet left a hairline of bare untinted material down both sides. An open completion drawer now drops the frost along with the band it belongs to. The drawer takes the band to 25% and blurs it while the native material stayed at full strength, which is the same bare slab in a different disguise. It mounts without a focus change, so it is observed rather than passed in, coalesced to a frame because the shell mutates with every streamed token.
Dictation, spoken replies, the wake word and start-conversation were four separate icon buttons in a Spotlight bar a few hundred pixels wide — most of the row spent on toggles that are set once and rarely touched. In the HUD they collapse into a single menu; the docked composer has the width and keeps them inline, same controls and same state. The trigger is not a static glyph. It reports the loudest live voice state — recording, transcribing, listening for the wake word, speaking replies — and lights while any is on, because a folded menu that looked idle with the mic open would be a worse trade than the space it saves. The three toggles are checkbox rows that hold the menu open on select, so the state you just changed is the state you can see. The shared control class names move to a module of their own so the row and the menus it renders can wear them without importing each other, and the pressed-toggle tint stops being written out at each of its four sites.
… above it The exit chip floated over the composer in a 26px transparent strip reserved for it (--hud-chip-strip), hidden until you hovered the bar. Under glass that strip is bare untinted material across the top of the HUD — a band of chrome above the surface, present in every state, holding a control you cannot see. It rides the composer's controls row now, next to send. That costs no reserved space and takes about 120 lines of CSS with it: the chip needed its own placement, hover reveal, leave-hold, and an opaque card to stay legible over an unknown desktop. None of that applies to a button on the bar, which is already our surface — the problem was the placement, not the control. Trade-off worth naming: the way out is now always visible in the HUD rather than revealed on hover. It is one more permanent glyph on a Spotlight bar, in exchange for an escape hatch that no longer depends on discovering it.
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
host.openSession awaited ensureGatewayProfile with no deadline. That await gates waitForFocusedSessionHydration, which arms the only timer on the path, so a profile dial that never settles left the open pending for the life of the window: the pane froze with no error, no Retry and - the part that made this hard to recognise - no timeout either. The gateway log signature is a bare `ws accepted` with no matching `ws closed`. Bound the activation with its own copy of the wake budget rather than folding it into the hydration one. A cold profile backend can legitimately spend most of the hydration budget painting a large transcript, and that race is already tight enough to lose, so charging activation to the same clock would trade a wedge for a regression. The timeout reuses the hydration message prefix on purpose - openSession keys the core stranded-session surface off it - and the [bot-wake] support log now names which phase expired, so a stuck dial is not read as a slow transcript. Scoped to callers that passed awaitHydration. A plain open never asked for a deadline and has nowhere to render one, so its behaviour is unchanged. Two existing tests counted microtask ticks between the call and the core open. The bounded activation adds a tick, so they now flush a macrotask instead, which asserts the same thing without depending on the await count. Refs NousResearch#89556
A review of the previous commit found that retrying at the plugin layer (openStoredBotChat catching and re-calling host.openSession) didn't fix the reported bug: host.openSession's own catch block unconditionally calls setResumeExhaustedSessionId on a hydration timeout before rethrowing, and only an explicit resumeSession() (the manual Retry button) clears that latch for the currently-routed session. A plugin-side retry is a different code path that can hydrate the transcript fine while the full-screen "Couldn't load this session" overlay stays latched over it. host.openSession now takes a retryHydrationTimeoutOnce option and retries the open+hydration-wait internally, before the latch is ever set, so a successful retry never arms the overlay. openStoredBotChat just opts in via that option.
Bot Mode passed keepAllProfilesScope:false, which re-homed the sidebar onto the bot profile. That profile forever-chat is hidden, so Sessions and the roster looked empty. Opening a bot is navigation, not a workspace switch. Also restore all-profiles when the bot backend is already live. Related: NousResearch#89789
Opening a plugin/Bot Mode session is navigation, not a workspace switch. keepAllProfilesScope (default true) now dials the named backend without moving $activeGatewayProfile or setApiRequestProfile. Session-owned RPCs still route to the session owner. Pass false to switch chrome and collapse the Sessions sidebar.
…ut a deadline Threading timeoutMs/signal through requestForSessionProfile and requestGatewayForProfile handed every session-scoped RPC a trailing `undefined, undefined`. Only the plugin host bridge actually supplies those, so the rest of the app's calls changed observed arity for no reason — and the resume/activate paths assert on the exact call shape. Forward the deadline args only when the caller set them; the plugin bridge keeps the full four-argument route it needs.
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
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.
Automated synchronization from
NousResearch/hermes-agent.This PR must pass the repository checks before it is merged. Release tagging is performed only after this PR is merged successfully.