feat(nous): send top-level session_id for provider sticky routing - #69253
Merged
Conversation
The Nous Portal profile only embedded the session id inside portal tags, so Claude traffic through the portal had no sticky-routing key. Multi-turn sessions could reroute between upstream endpoints (Anthropic/Vertex/ Bedrock), cold-writing a fresh prompt cache on every reroute since each provider's cache is instance-local. Mirror the OpenRouter profile: emit extra_body.session_id whenever the agent has one, pinning every turn of a session to the same endpoint so explicit cache_control breakpoints stay warm.
Contributor
૮ >ﻌ< ა ci reviewrunning on 87afd7a CI timingsCI timings · View jobWall time 7m27s vs 9m47s (-23.9%). 10 job(s) slower, 8 faster, 2 unchanged.
|
Sibling site of the profile change — the max-iterations summary path builds its request through the same NousProfile.build_extra_body(), so its exact-shape assertion now includes the sticky-routing session_id when the agent has a session.
x7peeps
added a commit
to x7peeps/hermes-agent
that referenced
this pull request
Jul 22, 2026
…ueue entries (#11) * feat(ui-tui): widget primitives — charts, accordion, shimmer, stable streams Reusable render primitives the SDK exposes to widget authors: sparkline/gauge/ hbars chart helpers (dimension-stable so live updates never resize the card), an Accordion for expand/collapse sections, animated shimmer loaders, and a streams demo that no longer reserves a phantom icon column on unfocused titles. * docs(skill): tui-widgets — auto-open recipe (openWidget at end of register) * feat(ui-tui): ambient zone system + widget crash boundary A full placement grid so the agent can put a widget where it asks — dock-top/ bottom and corner zones, with corners as reserved rails that take real space instead of floating over content. A per-widget error boundary plus lenient ShimmerRows means generated widget code can't crash the TUI. * refactor(ui-tui): host placement router + grid-test width-floor fix host.tsx collapses to one placement router over a shared render context, and the grid-test app drops its width floor too (carrying the #20379 review rule). Final formatting pass folded in. * feat(themes): cross-surface theme SDK — one skin themes CLI, TUI, and desktop Make the Python skin engine the single source of truth for a canonical theme shape consumed by every surface, so a skin authored in $HERMES_HOME/skins/*.yaml (by a user or by Hermes from a prompt) themes the CLI, TUI, and desktop GUI at once — the theme analogue of the plugin SDK. - @hermes/shared: canonical `HermesSkin` token shape + `SKIN_COLOR_TOKENS` enum, consumed by both TS surfaces (TUI `GatewaySkin` and desktop dedup onto it). - Desktop: `skinToDesktopTheme` resolver (skin → CSS-var palette, VS Code-style derive-from-seed) + `backend-sync` that registers backend skins into the theme registry (Appearance/Cmd-K/`/skin`) and applies on a real change. Seeds on gateway.ready (never stomps a persisted pick), applies on skin.changed and the post-turn `config.get skin` poll (catch-all for agent-edited config.yaml). - TUI: `fromSkin` now maps the status bar + `background` keys it was dropping. - Gateway: `config.get skin` also returns the full resolved palette (additive). - Skill: `hermes-themes` teaches the agent to author + activate a skin. Each surface keeps its own normalizing resolver (ansi for the TUI, CSS vars for the desktop, prompt_toolkit/Rich for the CLI). * fix(themes): activate skins via `hermes config set`, never a config.yaml hand-edit The skill told the agent to `patch` display.skin into config.yaml; a stray indent corrupts the file and breaks the live gateway (the reported "/ menu broke"), and a raw file edit never live-applies in a running CLI/TUI ("nothing happened"). Route activation through the safe writer (`hermes config set display.skin`), and state plainly that a tool call can't hot-switch a running CLI/TUI — the user runs `/skin <name>` (desktop still auto-repaints on the next turn). * feat(themes): agent-authored skins switch live via a gateway skin watcher A skin Hermes activates (`hermes config set display.skin X`) or recolors in place now goes live on every surface (CLI, TUI, desktop) within ~half a second, on its own — no `/skin`, no tool-hook timing, no user action. A gateway daemon polls the resolved skin signature `(name, active-file mtime)` every 0.5s and broadcasts `skin.changed` on any real move — a name switch OR a live color edit to the active skin. It routes through the SAME path `/skin` uses, so all surfaces repaint identically. The watcher seeds its baseline at gateway.ready (stdio + ws) so it only fires on a real change; the `/skin` RPC seeds the baseline too so it never double-broadcasts. Subsumes the desktop's post-turn `config.get skin` poll (its skin.changed handler already applies). * feat(themes): TUI paints its own background from the skin (OSC 11) The TUI inherited the terminal's background; now a skin's `background` paints the whole surface via OSC 11 when a skin is applied, and clears back to the terminal default (OSC 111) on revert and on exit (ridden in through resetTerminalModes). Opt-in: a skin with no `background` leaves the terminal untouched, and the restore only fires if we actually painted. Desktop already themed its own bg; this closes the loop so Hermes owns its background on every surface. * feat(themes): element tokens (ui_tool, ui_thinking) + skinnable diffs Theming was semantic-only: the gold tool `●` was `accent`, shared with headings/links/chevrons, so "recolor tool calls" was impossible and the agent had no key to point at. Add `ui_tool` (● + tool spinner) and `ui_thinking` (reasoning body) tokens that fall back to accent/muted — defaults unchanged, but now independently settable. Make diffs skinnable too (`diff_*`), which fromSkin previously hardcoded. Document the full element→key map in the skill so Hermes knows which knob turns what. * fix(themes): tweak the ACTIVE skin in place, never fork default Changing one color ("make the tool ● cyan") forked `default` — which has no `background` — so applying it reset the terminal to its own (black) default and dropped the active skin's palette. Teach the skill to edit the active skin's file in place for a tweak (watcher repaints on the mtime bump), and to fork a built-in only by carrying its full palette. Hard pitfall: never fork `default` for a tweak. * feat(themes): `hermes skin set` — deterministic one-color tweak, bg untouched Changing a single color kept wrecking the rest because the agent hand-authored a new skin (often from `default`, which has no `background`, resetting the terminal to black). Add `hermes skin set <key> <hex>`: edits the ACTIVE skin's one key in place (a built-in is forked into an editable copy carrying its full palette), so everything else — background included — is preserved. Plus `skin use` / `skin list`. The skill now points tweaks at this command instead of hand-authoring. * feat(themes): dedicated code-syntax palette keys Code highlighting reused brand tokens (accent/text/border/muted), so it couldn't be themed independently. Add syntax_string/number/keyword/comment skin keys → syntax* theme tokens (defaulting to those brand tokens, so defaults are unchanged) and point the highlighter at them. Documented in the element→key map. * test(themes): E2E live skin switch — config write → skin.changed broadcast * fix(themes): reconcile element/syntax tokens with main's derive+adapt pipeline Element tokens (ui_tool/ui_thinking), skinnable diffs, and code-syntax keys flow through buildPalette → adaptColorsToBackground instead of a hand-mapped color block, so they inherit #20379's contrast/polarity machinery. thinking and syntaxComment track the EFFECTIVE muted (banner_dim override included); the skin's `background` feeds the surface (it also paints the terminal via OSC 11); statusFg falls back through ui_text/banner_text. Tests assert the routing/independence contracts rather than pre-adaptation hexes. * fix(themes): apply a runtime switch back to default on the desktop ingestBackendSkin returned early for name === 'default' even when apply=true, so a real runtime switch to the default skin (/skin default on CLI/TUI, or config.set display.skin=default) emitted skin.changed but never repainted the desktop. 'default' is no-opinion on the PALETTE (the desktop keeps its own nous default, so we still never register a converted theme under it), but it IS a valid apply TARGET: setTheme normalizes 'default' -> nous, so switching back repaints to the desktop default. Skip only the registry step for 'default' and let it flow through the apply guard. Addresses Copilot review. * fix(tui_gateway): serve candidate-inclusive display on warm/live resume #65919 persists verification candidates (finish_reason=verification_required / verify_hook_continue) to state.db but collapses them out of the in-memory model history via repair_message_sequence. The eager session.resume + REST paths read the verbatim display lineage (candidate present), but the warm/live-reuse payload (_live_session_payload) built its user-visible messages from the collapsed in-memory model history — so switching to a still-live session dropped the substantive verification answer that a cold resume of the SAME session showed. That divergence is the cross-session "substantive text vanishes on switch" class, and the direct sibling of the resume-duplication regression fixed in #68149. Reconcile the persisted display lineage (candidate-inclusive, the same get_messages_as_conversation(..., include_ancestors=True) read the eager resume + REST paths use) with the fresh in-memory tail in _live_visible_history, so all three surfaces agree by construction while a not-yet-flushed live turn is still shown. Extracted _reconcile_display_with_live as a pure, DI-testable function (anchors on the last persisted row's (role, text); appends only the uncovered in-memory tail; trusts the DB display when the tail can't be anchored). Tests: unit coverage for candidate-inclusion, freshness, empty/raising-DB fallback, and the combined candidate+fresh-tail case. The existing freshness guard (test_session_resume_live_payload_uses_current_history_with_ancestors) stays green. * fix(tui_gateway): candidate-inclusive display on child-watch resume + E2E Complete the #65919 warm/live-payload fix across its sibling path and add real-SessionDB cross-builder coverage. - Child-watch (lazy) resume: the delegated-subagent watch window served _history_to_messages(repaired_history) for its user-visible messages, which collapses out persisted verification candidates just like the warm-payload path did. Build the visible messages from the verbatim child-only display projection (repair_alternation=False) while the repaired history still feeds live replay; fall back to the repaired history if the display read fails. - E2E cross-builder consistency (real SessionDB, not mocks): a persisted verification candidate is collapsed out of the model projection but kept in the display projection, and _live_visible_history now equals the eager session.resume display projection (candidate present). Adds the combined candidate + fully-flushed-second-turn case and a lazy child-watch handler test that asserts the candidate survives in resp["result"]["messages"]. * fix(cli): add skin to _BUILTIN_SUBCOMMANDS for plugin gating The new hermes skin subcommand must be declared so startup plugin discovery can skip when the user targets it. * fmt(js): `npm run fix` on merge (#69048) Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> * feat(desktop): Billing page revamp — current-plan card, in-app plans view, tier art (#68722) * feat(desktop): revamp Billing page — plan card, in-app plans view, tier art Reshape the desktop Billing settings per wayfinder ticket 09. New page order: Plan → Payment → One-time top-up → Automatic refill → Usage, with the at-a-glance summary strip unchanged at the top. - CurrentPlanCard replaces the old Subscription row: tier name + price + renewal and at most one button — "View plans" (free/no-sub + can_change_plan), "Change plan" (subscriber + can_change_plan), or none for teams / non-changers. Teams keep the portal "Adjust plan ↗" link so they are not stranded. The button navigates in-app to the plans sub-view. - bview=plans sub-view mirrors the settings pview/kview pattern (useRouteEnumParam, default overview). BillingPlansView renders a grid of PlanCard from live tiers[] (is_enabled, sorted by tier_order, free tier included). - PlanCard: tier art + name + $/mo + monthly credits as dollars ("$110 credits/mo"). Current tier is highlighted + inert; higher/no-current tiers get "Choose ↗" (opens portal with plan=<tierId>); lower tiers are a DISABLED "Downgrade" with a caption — downgrades move in-app in ticket 11 (gateway pending-change flow), so this PR intentionally links them out/disabled rather than wiring the money path. - buildManageSubscriptionUrl gains an optional third arg (tierId) → appends plan=<tierId>. Signature kept identical to draft PR #68666 for a trivial rebase; NAS #748 validates the param server-side. - Tier art: four NAS hero webps rendered as ~40px thumbnails over a Nous-blue well with per-tier blend modes (the only place Nous blue appears). Keyed by lowercase tier NAME (free/starter→connect, plus→memory, super→automation, ultra→sandbox); unknown name → text-only card. Imported via vite static imports for packaged file:// + webSecurity. - Top-up vs auto-refill disambiguated by section label + first sentence: "One-time top-up" / "Buy credits now" vs "Automatic refill" / "Refill when low" (configured copy reads "Charges $X automatically when your balance falls below $Y."). - Variant-A auto-refill editing: Manage swaps the row's left side (caption → the two $ fields with a pre-allocated error line) and the action column (Manage → Save/ Cancel) in place, with the row height reserved for the tallest state so the Usage section never shifts. Fixes the spurious on-open validation error (errors now show only after an edit or a save attempt). Save/disable API calls + confirm-disable flow unchanged. - Remove subscriptionTierChips and the subscription-row chips; reshape (not delete) deriveBillingView to expose plan + tiers. Buy-credits row keeps the chips seam. - Dev fixtures: add free-personal and subscriber-personal (personal orgs, full 4-tier Free/Plus/Super/Ultra catalog) so the plans view is exercisable. Tests: update/extend index.test.tsx + use-billing-state.test.ts, add tier-art.test.ts; delete the old chips tests. Desktop billing suite 70/70 green, typecheck clean. * fix(desktop): mark the free/lowest tier current (not an upgrade) when there is no subscription Visual verification caught a spec-fidelity bug: in the plans grid, an account with no active subscription rendered the Free tier ($0/mo, tier_order 0) as a "Choose ↗" upgrade — clicking would deep-link the portal to "subscribe to Free". Ruling: current-card = tier.is_current OR (subscription.current == null AND the tier is the lowest-order / $0 tier). derivePlanTiers now falls back to the lowest-order tier as the stand-in current plan when there is no subscription, so the free card renders exactly like is_current (inert, "Current plan") and — being the lowest order — no tier can be a downgrade; every paid tier is a "Choose ↗" upgrade. CurrentPlanCard is unaffected (still "Free" + "View plans"); subscriber-personal is unchanged (Free stays a disabled Downgrade below the current Plus tier). Tests: free-personal grid now asserts Free = current/inert, no downgrade state, three Choose buttons; text-only unknown-tier test gains a free tier so the unknown paid tier is unambiguously an upgrade. Billing suite 70/70 green, typecheck + lint clean. * chore(desktop): shrink bundled tier art to 128px thumbnails The plan-card wells render the art at ~40px; shipping the full landing images added 2.7 MB to the repo for no visible difference. 128px covers 2x displays; total is now 26 KB. * fix(desktop): address 6 adversarial-review findings on the Billing revamp 1. Grandfathered current tier (BLOCKER). NAS marks a grandfathered current tier is_enabled:false; the enabled-only filter dropped it, leaving currentOrder undefined so every lower tier rendered as an actionable "Choose ↗". derivePlanTiers now resolves current identity/ordering against the UNFILTERED tiers and keeps the grandfathered current tier in the grid as the inert "Current plan" card; downgrades classify against its tier_order. (Non-current disabled tiers are still dropped.) 2. Dead plan-card button. derivePlanCard offered "View plans"/"Change plan" purely on can_change_plan, but the grid could be empty / current-only and showPlans refused, so the button no-oped. It now offers the in-app action ONLY when the grid has ≥1 actionable (non-current) tier; otherwise it falls back to the portal link. 3. Deep-link bypass. showPlans now gates on the same capability that renders the button (view.plan?.action), so a team / non-changer deep-linking bview=plans always falls back to overview instead of a grid of live Choose buttons. 4. Lost portal escape hatch. Whenever the card has no in-app action (teams, non-changers, refused subscription, empty catalog) it now ALWAYS carries the "Adjust plan ↗" portal link built from subscription?.portal_url ?? billing.portal_url — the refusal caption no longer promises a portal the UI didn't render. 5. Choose URLs dropping org_id/plan. (a) derivePlanTiers now threads billing.portal_url as the fallback base for the Choose URL. (b) buildManageSubscriptionUrl treats the hard-coded FALLBACK_PORTAL_BILLING_URL as a last-resort ORIGIN (applying org_id/plan) instead of a bare return, so a null portal_url never strips the routing params. 6. Zero-shift on narrow panes. Replaced the magic min-h-28 (under-reserved once the two inputs stack below @2xl) with exact reservation: the edit form is always rendered and both states share one grid cell ([grid-template-areas:'stack']), invisible+aria-hidden when not editing — the row equals the tallest state at every width, no breakpoint math. The refusal stays inside the reserved layer. Tests: +12 (grandfathered current, no-dead-button + empty-catalog portal link, team & personal deep-link fallback to overview, billing.portal_url-backed Choose URL, fallback org_id/plan, reserved-form-mounted); updated the two portal-link expectations for §4. Billing suite 78/78 green; typecheck (app/electron/e2e) + lint clean. * refactor(desktop): reuse the shared openExternalLink helper in the plans view * fix(desktop): honor the auto_reload wire contract — null card + disable amounts A full-stack contract sweep (desktop ↔ shared types ↔ gateway ↔ NAS) surfaced two real desktop bugs in the auto-refill row: A. auto_reload.card can be null. The gateway's _parse_auto_reload_card returns None for a missing/unknown-kind card and _serialize_billing_state emits `card: null`, but the shared BillingAutoReload.card union had no null arm and use-billing-state dereferenced `autoReload.card.kind` bare — a crash on the enabled path. Add `| null` to the shared union (contract honesty) and guard the read (`card?.kind`); null now falls through to the default enabled path, same as a canonical card. B. Disable was rejected by the gateway. billing.auto_reload unconditionally requires threshold + top_up_amount, so `updateAutoReload({ enabled: false })` came back invalid_request. (The TUI always sends both; desktop fixture mode stubbed it.) disable() now sends the current threshold_usd/reload_to_usd from the autoReload prop alongside enabled: false, matching the TUI. Tests: enabled auto_reload with card:null renders the normal enabled row (derivation + render, no crash); disable call carries both current amounts. Billing suite 80/80 green; typecheck (app/electron/e2e) + lint clean. * fix(tui): guard the nullable auto_reload card in the auto-reload screen The shared BillingAutoReload.card union gained its honest null arm (the gateway emits card: null for a missing/unknown card); the TUI's only bare dereference follows the same default path as a canonical card. * fix(desktop): align billing inputs to the sm control height The three billing inputs used an ad-hoc h-8 (32px) next to size=sm buttons (24px). They now use the control system's size=sm with a py-[3px] compensation for the input's real 1px border — buttons draw theirs as an inset shadow, so sm alone still sits 2px taller. All five controls in the buy row now measure 24px. * fix(desktop): plan-card actionability + billing view-model hardening Code-quality review of the Billing revamp (PR #68722). BLOCKING — a top-tier subscriber (only downgrades/current below them) opened a plans grid with zero enabled actions AND no portal link. The plan card gated its in-app button on `tiers.some(state !== 'current')`, which counts the (disabled) downgrade tiles. It now gates on an actual UPGRADE being present (`capable && tiers.some(state === 'upgrade')`); with no upgrade the card falls back to its "Adjust plan ↗" portal link, and the bview=plans deep link (gated on the same plan.action) falls back to overview. Reviewer structural items: - One "plans capability" verdict (personal + can_change_plan + subscription ok) is derived once in deriveBillingView and threaded to BOTH derivePlanCard and derivePlanTiers; the grid only mints upgrade actions when capable, so the invariant lives in one place. - BillingPlanTierView is now a discriminated union (`current` | `downgrade` w/ disabledCaption | `upgrade` w/ required action), and BillingPlanCardView is an action-XOR-link union — deleting the `tier.action?.url ?? ''` and `plan.link?.url` defensive branches in the consumers. - `findCurrentTier(subscription)` replaces the repeated is_current||id predicate at its three sites (plan card price, grid ordering, summary plan line). - BillingView exposes named `paymentRow` / `topupRow` / `refillRow` instead of an `accountRows[]` + three `.find(id)` lookups. - The auto-refill row that edits in place carries an explicit `manageInApp: true`; AutoReloadRow keys off it instead of sniffing the action label/url. - tier-art header comment no longer cites an internal repo path; dead `?.` removed from RowValue (via a destructured const) and the plan-card link handler. Behavior is identical except the blocking fix. Billing suite 82/82 green; typecheck (app/electron/e2e) + lint clean. * refactor(desktop): adopt inline-review nits on the billing plan card Resolves the inline suggestion threads: - plan-card gate reads a named `hasActionableTier` = "a tile carries an action" (union-safe `'action' in tier`, equivalent to the old upgrade-only check). - re-narrow link/action inside the click callbacks (`plan.link && …`, `tier.action && …`) rather than relying on outer narrowing. Behavior unchanged; billing suite 82/82 green, typecheck + lint clean. * fmt(js): `npm run fix` on merge (#69050) Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> * feat(cli): plan catalog on Free + plan= deep link + top-up/auto-refill copy split (#68689) * feat(cli): plan catalog on Free + plan= deep link + top-up/auto-refill copy split Bring the plain (non-TUI) CLI billing surface to parity with the desktop/TUI billing changes: - /subscription on Free (admin/owner, interactive) prints the plan catalog (name · $/mo · $credits/mo, from the same tiers[] data the TUI uses; monthly credits render as dollars). A numbered pick opens the manage-subscription deep-link directly with plan=<tier_id> appended. - subscription_manage_url(state, tier_id=...) appends plan=<tier_id> (the stable tiers[] id) when a tier was picked, org_id first — mirrors the TUI's ?plan=. The paid change flow's blocked/unknown-preview portal fallback carries plan= for upgrades only; downgrades stay generic/native. - /topup overview splits one-time top-up from automatic refill, the distinction stated in each first sentence ("Add funds now — a single charge…" vs "Refill when low — charges … automatically …"), keeping "credits" out of the dollars-only surface. - Downgrades remain native (chargeless scheduled change), unchanged. Updates the CLI-parity section of docs/billing-lifecycle.md and tests under tests/hermes_cli + tests/agent. * refactor(billing): share plan-catalog helpers + harden manage-url builder - subscription_manage_url now preserves unrelated portal query params (parse_qsl, popping only the contract-owned org_id/plan) and restricts to http/https schemes, matching the desktop URL builder — the function owns the contract. - Lift the plan-catalog derivation into agent/subscription_view.py so the CLI Free catalog and the paid picker/blocked-preview branch share one implementation: selectable_tiers (enabled paid, not current, sorted), format_tier_row (name · $/mo · $credits/mo — thousands-grouped like the TUI's toLocaleString, credits suffix hidden when absent/zero), and is_upgrade(state, tier_id). * fix(cli): numbered pick, canonical guarded browser opener, partial auto-refill copy - Free catalog: accept a bare digit as a pick (the shared normalizer only knows the confirm-dialog digit aliases, so `1` used to resolve to None → "Cancelled"). The Nth digit maps to the Nth printed row. - Extract one _open_url_in_browser used by every "open the portal" path, applying the device-code flows' console-browser / remote-session guard (webbrowser.open returns True even for lynx/w3m over SSH) and returning whether a real browser opened. - Consume the shared selectable_tiers / format_tier_row / is_upgrade helpers from the Free catalog, the paid picker, and the blocked-preview branch. - /topup auto-refill copy: the concrete "charges $X … below $Y." sentence only when both amounts are present and finite; otherwise the generic sentence. * docs(billing): correct CLI-parity rows (drop cross-repo ref, downgrade invariant) Remove the other-repo PR reference from the manage-URL row, and state the real downgrade invariant: a blocked downgrade may print the generic manage URL but never carries plan=<tier_id> — selected-tier deep-links are reserved for new subscriptions and upgrades. * feat(desktop): native in-app downgrade — chargeless preview → schedule → undo (#68761) * feat(desktop): native in-app downgrade (chargeless preview → schedule → undo) Ticket 11, stacked on the Billing revamp (ticket 09). Downgrades no longer bounce to the portal — picking a lower tier runs the gateway pending-change flow in-app; the scheduled state renders on the plan card with an undo. Upgrades keep the portal deep link. - api.ts: add previewSubscriptionChange / scheduleSubscriptionChange / resumeSubscription wrappers over subscription.preview|change|resume ({subscription_type_id} / {}), typed via SubscriptionPreviewResponse + BillingMutationResponse (now re-exported from types.ts). - use-subscription-change.ts (new): useDowngradeFlow (preview → confirm → schedule, refetch + onScheduled on success; typed refusals surface via the shared BillingRefusalInline, so insufficient_scope drives the existing step-up exactly like the auto-reload save, retried in place) and useResumeFlow (confirm-less undo). Both accept a `simulate` switch so DEV fixtures click through with canned success. - plans-view.tsx: downgrade tiles are now an actionable "Downgrade" that opens an in-card preview → confirm panel (mirrors the TUI confirm copy: "…takes effect <date>. No charge now; you keep your current plan until then."). The scheduled downgrade target renders an inert "Scheduled" marker; other lower tiers stay actionable (picking one reschedules). - CurrentPlanCard: when a downgrade is pending, the caption reads "Changes to <tier> on <when>." with an inline Undo → resume → refetch. One line, no jumps. - use-billing-state.ts: BillingPlanTierView gains a `scheduled` state (and drops the ticket-09 disabled-downgrade caption); derivePlanTiers matches the pending target by name (NAS sends no id for it) before the downgrade branch; BillingPlanCardView gains `pending`, derived from current.pending_downgrade_* . - inline-feedback.tsx (new): extracted openExternal / BillingRefusalInline / StepUpInlineAction / InlineMessage so the plans view reuses the step-up-aware refusal renderer without a circular import; openExternal now delegates to the canonical @/lib/external-link opener. - dev-fixtures.ts: add `pending-downgrade` (subscriber-personal on Plus with a Free downgrade scheduled for Aug 15) for the plan-card pending state + grid marker. Tests (+16 → 94 green in the billing suite): api wrappers (preview/change/resume + insufficient_scope refusal); view derivation (pending plan-card state, scheduled grid marker); confirm flow (preview shown, change called with the right tier_id, refetch on success, schedule refusal → step-up affordance); undo flow; the use-subscription-change hooks (preview-refusal retry, cancel, simulate path). Updated the ticket-09 downgrade tests for the now-actionable tile. typecheck (app/electron/e2e) + lint clean. PR (later): base sid/desktop-billing-revamp; retarget to main after #68722 (09) merges. * fix(desktop): format downgrade credits delta as signed dollars The downgrade preview rendered the raw wire string ("Monthly credits change: -88."), violating the "monthly credits are DOLLARS" ruling. NAS sends monthly_credits_delta as a bare decimal; format it as signed dollars through the same money formatter ("−$88/mo", sign preserved, abs value formatted). Zero / absent still hides the line. Adds formatMonthlyCreditsDelta (exported) + unit tests (negative/positive/zero/ absent) and asserts the rendered "Monthly credits change: −$88/mo." in the confirm flow. Billing suite 99/99 green; typecheck + lint clean. * fix(desktop): downgrade flow hardening — concurrency guard, a11y, DEV-gated sim Addresses the adversarial review of the native-downgrade diff. - Concurrency: useDowngradeFlow exposes `mutating` (true only while the schedule RPC is in flight). While a change commits, every other Downgrade tile and the Back button are disabled; the active panel's Confirm/Cancel already lock. The plan-card Undo blocks on its own resume via `busy`. (The server also 409s overlapping per-org mutations — this is UI honesty, not the only defense.) - Accessibility: the confirm panel is role="status" aria-live="polite" and takes focus on open (tabIndex=-1 container); closing it returns focus to the tile card, so keyboard focus is never stranded and the async preview text is announced. - DEV-gated simulation: the canned preview/change/resume seam is ignored unless import.meta.env.DEV, so a production build never takes the simulated branch even if a `simulate` prop leaks through. - Comments: documented the deliberate manual-retry-after-step-up (no auto-replay, matching auto-reload) and that name-matching the scheduled target is safe because SubscriptionTypes.name is @unique in NAS. Tests (+5 → 104 green in the billing suite): mutating exposed only during schedule; simulate ignored outside DEV; other downgrade tiles + Back disabled mid-schedule; Undo disabled mid-resume; confirm panel role + focus on open. typecheck (app/electron/e2e) + lint clean. * fix(desktop): scheduled cancellations, downgrade-flow concurrency, inline nits Addresses the native-downgrade review threads. Scheduled cancellations were invisible (NEW review item). subscription.current carries cancel_at_period_end + cancellation_effective_* and subscription.resume clears cancellations exactly like downgrades, but the pending-transition helper only read pending_downgrade_*, so a portal/TUI-scheduled cancellation rendered as a plain renewal with no Undo. The pending state is now a union — { kind:'downgrade', tierName, when } | { kind:'cancellation', when } — computed once in deriveBillingView and threaded to BOTH the plan card and the grid. The card reads "Cancels on <date>." with the same Undo (resume); the grid shows a Scheduled marker only for downgrades (a cancellation has no target tier). Precedence: a downgrade wins if both fields are set (it names a concrete target — the stronger signal), commented at the helper. Adds a `pending-cancellation` fixture + tests (card copy, undo wiring, no grid marker, downgrade-wins precedence). Concurrency: confirm() takes a synchronous scheduling ref (mirroring useResumeFlow) so two same-tick clicks — before React commits busy='schedule' — cannot fire two schedule RPCs; the ref clears on every exit (simulated/stale/refusal/success). useResumeFlow reorders its unlock: a refusal releases immediately, a success holds runningRef/busy THROUGH the refetch so Undo never re-enables against the still-pending card. Test: a synchronous double-activation fires one schedule RPC. Inline nits: re-narrow link/action inside the click callbacks (`plan.link && …`, `tier.action && …`) instead of relying on outer narrowing / `?? ''`. Billing suite green (109); typecheck (app/electron/e2e) + lint clean. * refactor(desktop): move DEV billing simulation behind the api seam The fixture simulation lived as `simulate` / `simulateResume` prop drills and `if (simulated)` branches inside the flow hooks, and it could not actually produce the state it advertised (a simulated schedule never showed the pending card). Replaced with `createSimulatedBillingApi(fixture)` — a fully in-memory BillingApi built once, DEV-gated, in BillingSettingsWithDevFixtures where the fixture is known, and supplied to the whole subtree via a new `BillingApiProvider` (context override on `useBillingApi`; `null` = the real gateway api). It serves fetches from a mutable copy of the fixture and its subscription-change mutations WRITE that copy's pending state: schedule sets a pending downgrade, resume clears a pending downgrade OR cancellation. Fixture mode now flows through the SAME react-query path (fetch short-circuit deleted; queries always enabled; an effect refetches on fixture switch), so the click-through genuinely progresses — schedule → pending card + Undo + Scheduled marker, undo → cleared. Deleted `SubscriptionSimulation`, `simulationEnabled`, both prop drills, and every `if (simulated)` branch — the hooks are now production-pure. Added a test driving the full simulated loop (schedule → pending appears → resume → cleared), plus cancellation undo and no-shared-mutation coverage. Removed the now-obsolete simulate hook tests. Billing suite green (110); typecheck (app/electron/e2e) + lint clean. * refactor(desktop): extract billing row/card components out of index.tsx Purely mechanical, no behavior change: split the settings billing route file (1065 → 593 lines) into focused siblings now that the downgrade feature has settled their final shape. - billing-amounts.ts — the dollar parse/format/validate/clamp helpers. - account-row-value.tsx — RowValue (shared by AccountRow + AutoReloadRow). - current-plan-card.tsx — CurrentPlanCard. - auto-reload-row.tsx — AutoReloadRow (the in-place auto-refill editor). index.tsx keeps the page shell, AccountRow dispatch, BuyCredits flow, and the fixture wiring. Billing suite green (110); typecheck (app/electron/e2e) + lint clean. * refactor(desktop): tighten the downgrade flow — phase union, previewMessage, tidy shared modules Polish that composes with the api-seam rework: - ActiveDowngrade's four nullables become a `DowngradePhase` discriminated union (previewing | previewFailed | ready | scheduling | scheduleFailed). Impossible combinations (a preview AND a refusal, "ready" with no quote) can no longer be represented; the hook and panel branch on one `kind`, and `mutating` is simply `phase.kind === 'scheduling'`. - The five-way ternary in DowngradeConfirm is replaced by a pure `previewMessage(phase, fallbackTierName)` helper; the misnamed `caption` className local is renamed `captionCn`. - inline-feedback.tsx now holds ONLY the shared refusal/step-up pieces: `openExternal` moves to its own `open-external.ts` (a thin wrapper over `@/lib/external-link`'s `openExternalLink`), and `InlineMessage` moves back into its sole consumer (auto-reload-row.tsx). No behavior change. Billing suite green (110); typecheck (app/electron/e2e) + lint clean. * fmt(js): `npm run fix` on merge (#69067) Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> * fix(desktop): feed memory.provider dropdown from live discovery The desktop Settings memory-provider dropdown read a hardcoded `ENUM_OPTIONS['memory.provider'] = ['', 'honcho', 'hindsight']` list, so user-installed and pip-installed providers never appeared even though the backend already discovers them (`GET /api/memory` -> `_discover_memory_provider_statuses()`) and the CLI (`hermes memory setup`) lists them. This was the one surface left where the memory config stack was not schema/discovery-driven. Fetch `getMemoryStatus()` on the settings page (mirroring the existing `elevenLabsVoiceOptions` pattern) and pass the discovered provider names to `enumOptionsFor` as `dynamicOptions` for the `memory.provider` key. The static `ENUM_OPTIONS` entry is demoted to a pre-load fallback; the current-value passthrough still keeps a selected-but-undiscovered provider visible. Completes the desktop half of the schema-driven memory-provider config surface (the CLI + backend + generic panel already landed via #51020 / #67206), superseding the stale #48675 which built the same feature against the pre-refactor layout. Co-authored-by: brooklyn! <770929+OutThisLife@users.noreply.github.com> * refactor: make memory.provider schema-driven instead of a 2nd fetch Addresses review on #69077. The first pass added a second, heavier round-trip (`GET /api/memory` -> `_discover_memory_provider_statuses()`, which imports every provider module and probes install state) just to fill the desktop dropdown, and left `schema.options` for memory.provider dead — three sources of truth for one list. Root cause is narrower: the desktop schema *already* carried a discovery-driven `memory.provider` option list (`_SCHEMA_OVERRIDES` -> `_memory_provider_options()`), but `enumOptionsFor` returned the static `ENUM_OPTIONS['memory.provider']`, which shadowed `schema.options` in config-field.tsx. The only real gap was liveness: `_SCHEMA_OVERRIDES` is frozen at import time, so a provider installed mid-session never showed. Fix at the layer the rest of this stack already uses: - Backend: generalize `_schema_with_voice_provider_options` -> `_schema_with_dynamic_provider_options`, which now also recomputes `memory.provider` options per request (cheap plugin-dir scan via `_memory_provider_options`, plus current-value preservation). Fixes the same staleness for CLI + dashboard, not just desktop. - Frontend: drop the `memory.provider` entry from `ENUM_OPTIONS` so `enumOptionsFor` returns undefined and config-field consumes the discovery-driven `schema.options` directly. No new frontend round-trips. - Remove the now-unnecessary `getMemoryStatus()` fetch/state/wiring in config-settings.tsx (reverted to main). - Fix the stale `helpers.ts` comment ("schema omits memory.provider"). Tests: backend tests for the per-request merge (recomputes discovered providers; preserves a configured-but-undiscovered value); frontend test asserts enumOptionsFor no longer shadows the schema for memory.provider. Co-authored-by: brooklyn! <770929+OutThisLife@users.noreply.github.com> * refactor: tidy dynamic schema-options merge Cleanup pass on the per-request provider-options merge — behavior unchanged: - collapse the duplicated entry-validation shared by merge() and its callers into a single guard inside merge() - read the configured memory provider in readable steps instead of a nested ternary - build the merged mapping as one {**base, **overlay} expression - space out logical blocks * fix(gateway): hard-exit CLI runner after graceful teardown * test: update gateway run stub for hard-exit helper * fix(gateway): hard-exit on KeyboardInterrupt path too The KeyboardInterrupt handler in run_gateway() was the only exit path that still used bare 'return' instead of _hard_exit_after_gateway_teardown(). While less common than service-managed restarts, a console Ctrl+C still leaves the process vulnerable to the same Python finalization hang on non-daemon worker threads (cron ThreadPoolExecutor jobs). Route it through the same backstop, with a 'return' guard for test stubs that don't raise on code 0 (production os._exit never returns). * fix(cli): pass conversation_history on /new /resume /branch flush Closes #68454 Root cause: cold-resumed transcript rows lack _DB_PERSISTED_MARKER until a normal turn flush stamps them. Immediate /new,/resume,/branch flushed with no history boundary, so every restored row was re-appended to the old session. Fix: pass conversation_history=self.conversation_history at all three sites (mirrors #68205). Add offline regression coverage for noop + tail-only write. Verification: pytest tests/agent/test_session_rotation_flush_cold_resume_68454.py (4 passed) * test: drop source-grep change-detector from #68480 The three behavior tests (control proves dup, boundary is noop, tail-only write) fully cover the flush semantics. The source-grep test reading cli.py + cli_commands_mixin.py as text and asserting a string appears is a change-detector that breaks on benign refactors without adding coverage. * test: update mock assertions for conversation_history kwarg The /branch and /resume flush tests asserted the old positional-only call signature. Update to match the fix from #68480. * fix(gateway): make adapter fatal-error handoff cancellation-proof; exit if a platform is stranded The fatal-error notification runs on the failing adapter's own polling task, and adapter.disconnect() inside the handler can cancel that task (its current-task guard misses because _safe_adapter_disconnect runs the close in a wrapper task). The CancelledError killed the handler between the fatal log and the reconnect queue, leaving the platform permanently dead inside a live gateway process. #68447 fixed this for telegram at the adapter layer; this hardens the shared gateway dispatch so every platform gets the same protection (qqbot #25505/#29005, photon #68693). - _handle_adapter_fatal_error now runs the real handler in a detached task, awaited through asyncio.shield() so caller cancellation cannot tunnel into it (Task.cancel() also cancels the task's _fut_waiter). - If a retryable platform still ends up neither reconnected nor queued, the gateway exits with failure so launchd/systemd KeepAlive restarts it instead of running indefinitely with a dead platform (#68693). Fixes #68693 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore: map anoop.mehendale@gmail.com -> anoopmehendale-cue For PR #69007 salvage (#69112). * fix(update): isolate systemctl timeouts per gateway unit during fleet restart A TimeoutExpired from one hermes-gateway*.service used to abort the whole per-scope restart loop, leaving later profile gateways on pre-update in-memory code after hermes update. Catch timeouts per unit, continue the fleet, warn with the exact stale units, and exit non-zero when any remain unrestarted (#68523). * test(update): cover fleet restart timeout isolation (#68523) * fix: refresh vulnerable npm lockfile entries * chore: AUTHOR_MAP for tinetwork * fix(compression): prevent stale-budget retry loops * fix(compression): harden startup route scoping * fix(providers): align custom route scoping * test(compression): cover overflow after blocked preflight * test(providers): cover route URL identity boundaries * test(providers): cover query path slash identity * test(providers): complete hermetic route coverage * test(providers): cover URL whitespace route identity * fix(providers): fail closed on missing active route * fix(providers): scope route-owned runtime settings * fix: restore base_url rstrip, extract should_clear_context_pin helper Salvage follow-up for PR #68899: - Restore .rstrip('/') on base_url in _swap_credential (both anthropic and OpenAI paths) to match every other assignment site. The route identity comparison still uses normalize_route_base_url which handles trailing slash correctly. - Extract should_clear_context_pin() into hermes_cli/route_identity.py, consolidating 7 copy-pasted call sites across cli.py, gateway/run.py, gateway/slash_commands.py, and hermes_cli/model_switch.py into a single fail-closed helper. C1 (anthropic path TLS re-application): pre-existing gap — the Anthropic adapter (build_anthropic_client) has no TLS customization support at all, so this is out of scope for this salvage. * fix(compression): ignore assistant handoff summaries in tail anchor Assistant-role compaction summaries were treated as the last visible assistant reply after head protection decayed. That pulled the tail boundary back to the summary itself and left zero new turns to summarize. Exclude internal context summaries from both the visible-reply search and the assistant fallback, mirroring the existing user-role summary exclusion. * chore: AUTHOR_MAP for McHermes * fix(telegram): group authz fallback + command sender identity - authz_mixin: add config.extra fallback for group_allowed_chats when observe-unmentioned mode strips user_id from env-var check - authz_mixin: check adapter allow_from/group_allow_from for user authorization from config.yaml without env vars - telegram/adapter: separate group_allow_from for group chats vs allow_from for DMs - telegram/adapter: preserve sender source for command messages so admin-only slash commands work in groups - telegram/adapter: add _telegram_extra fallback for group_allow_from config reading * fix(telegram): address review findings from PR #67816 - Update test_observed_group_context_preserves_slash_command_text_for_dispatch to assert user_id is preserved for COMMAND messages (new correct behavior) - Add _coerce_allow_set helper to handle both list and comma-separated string allowlist inputs (prevents character-by-character iteration bug) - Include 'channel' in chat_type checks for group-scoped authorization - Add _telegram_extra fallback for group_allowed_chats (consistent with group_allow_from fallback) - Add AUTHOR_MAP entry for nyaruko@hermes -> tsuk1nose * fix(telegram): update auth check tests for group_allow_from split Update test_telegram_auth_check.py to use group_allow_from for group messages (matching the PR's intentional behavior split: allow_from for DMs, group_allow_from for groups). Add test_is_user_authorized_from_message_group_allow_from to cover the new group path. * fix(openviking): recover pending session commits * docs: clarify OpenViking local setup (cherry picked from commit a6807170f109dfaab19bc2023ddb5bb33fcb2852) (cherry picked from commit 6fb4e9aa8a42967a5c25e53ad3c969e81e6da4f4) * fix(openviking): serialize orphan session recovery * fix(openviking): chunk structured session sync Preserve ordered structured turns across OpenViking's 100-message batch limit and resume retries from the first unconfirmed message. Based on the OpenViking batching work from commit 1a567f706703b8005e3fb915548f8a3cf137e581 in #58981. * refactor: cleanup follow-up for salvaged PR #58871 - Remove dead current_sid parameter from _recover_pending_sessions - Remove dead cleanup parameter from _release_owner_run_claim (always True) - Set _run_lock_path after flock succeeds, not before - Collapse redundant BlockingIOError branch (covered by OSError+errno check) - Track _pending_marked_sids to skip re-writing marker file on every sync_turn * fix(openviking): inject session-start memory context (cherry picked from commit 18b474d0bd2144f9507c32a3cecbed0fb5620617) * fix(openviking): align session context with shared profile contract * fix: discard both session IDs on compression for profile re-injection The _profile_prefetched_sessions set stores whichever session_id was passed to prefetch(), which may differ from self._session_id. On compression, only old_session_id (self._session_id) was discarded, missing the case where the stored key was the prefetch session_id parameter. Discard both old and new IDs to cover all cases. * chore: add kshitij@kshitij.dev to AUTHOR_MAP * fix(secrets): fall back to stale disk cache when bws live fetch fails Without this, a single DNS hiccup or BWS outage at gateway startup leaves the whole fleet running with an empty credential pool — every model call fails until someone restarts after the network recovers. When a previous successful fetch already populated the disk cache, return those secrets with an explicit warning instead of raising RuntimeError. `use_cache=False` (explicit opt-out) still raises so manual flows like the setup wizard surface the original error. The disk cache is not re-written on the fallback path so a process restart still triggers a proper TTL re-check. Fixes #41925 * fix(secrets): port stale-cache fallback to current DiskCache API + gate by error kind The stale-fallback branch called _read_disk_cache(), a helper removed in db495b0fbaaa63ebd7f6404413730f98f0fdf76b when disk-cache logic moved to the shared DiskCache class — every fallback attempt raised NameError instead of serving cached secrets, silently defeating the PR's whole purpose. Port to _DISK_CACHE.read(). Also tighten the fallback per DiskCache's TTL contract and the secret-source error taxonomy: - Gate on cache_ttl_seconds > 0 so a caller that opted out of caching entirely (ttl=0) never gets a secret value that didn't come from a live fetch, even on the failure path. - Gate on _classify_bws_error(str(exc)) being NETWORK or TIMEOUT, reusing the existing classifier — an AUTH_FAILED or malformed-output failure must still raise, since serving stale secrets there would mask a real credential/config problem instead of a transient outage. Ported the test helpers off the removed _write_disk_cache to a direct JSON write (matching this file's existing disk-cache test convention) and added tests for the auth-failure, malformed-output, and zero-TTL gates. Reverting the fix and re-running confirms 7 of 8 stale-fallback tests fail with the original NameError. * fix(secrets): fold OP_CONNECT_HOST/OP_CONNECT_TOKEN into 1Password auth cache-key _auth_fingerprint() built the 1Password secret cache-key from the service-account token, OP_ACCOUNT, and OP_SESSION_* vars but omitted OP_CONNECT_HOST/OP_CONNECT_TOKEN, which are in _OP_ENV_ALLOWLIST and are forwarded to the op child (the Connect-server auth path). Rotating OP_CONNECT_TOKEN or re-pointing OP_CONNECT_HOST at a different Connect identity left the fingerprint unchanged, so both the in-process and disk caches kept serving secrets resolved under the old Connect credentials for the full TTL (default 300s, disk-persisted across invocations). This contradicts the function's own docstring invariant that a value cached under a previous identity is never served under a new one; it closes the gap for the Connect path, matching the OP_SESSION_*/service-account paths that are already protected. * fix(secrets): pass OP_LOAD_DESKTOP_APP_SETTINGS through to the op child env The 1Password secret source builds a minimal allowlisted environment for the `op read` child process. The allowlist omits OP_LOAD_DESKTOP_APP_SETTINGS, so a user who exports it (shell, .env, or service unit) sees it silently stripped before it reaches `op`. That var is `op`'s documented switch to skip the desktop-app integration probe. When the 1Password desktop app is installed, `op` probes its settings/socket at startup *before* evaluating service-account auth. If the desktop app's group container is wedged (e.g. macOS 'Interrupted system call' on the 1Password group container), that probe blocks with no timeout, so `op read` hangs indefinitely even with a valid OP_SERVICE_ACCOUNT_TOKEN present. Setting OP_LOAD_DESKTOP_APP_SETTINGS=false is the intended escape hatch — but stripping it means it has no effect on exactly the headless boxes that need it. Fix: add OP_LOAD_DESKTOP_APP_SETTINGS to _OP_ENV_ALLOWLIST so the documented var reaches the child. No behavior change when it's unset. Adds a focused test alongside the existing allowlist test. Repro: on a machine with a wedged 1Password desktop container + a valid SA token, `op read` hangs 600s+ without the var and returns in ~4s with it — but only if it actually reaches the op process, which this allowlist entry ensures. Co-authored-by: Minh Nguyen <menhguin@users.noreply.github.com> * fix(mcp): pass secret-source-injected env vars to stdio servers Surgical reapply of PR #37523 onto current main (the original branch predates the SecretSource registry refactor). _build_safe_env() now forwards env vars tagged in env_loader._SECRET_SOURCES — widened from Bitwarden-only to any registered secret source (Bitwarden, 1Password, plugin backends), since the provenance map is source-agnostic. Explicit server env: config still wins; untagged secrets stay filtered. Fixes #37499. * fix(env): stop printing Bitwarden secret names * fix(secrets): validate bitwarden status token Keep the env-presence row, but add a real Bitwarden probe so revoked or malformed tokens no longer look healthy in hermes secrets bitwarden status. Also document the new status behavior and lock it in with a dedicated regression test. Refs: NousResearch/hermes-agent#40275 Tested: ./scripts/run_tests.sh tests/hermes_cli/test_bitwarden_status.py tests/test_bitwarden_secrets.py Tested: .venv/bin/python -m ruff check hermes_cli/secrets_cli.py tests/hermes_cli/test_bitwarden_status.py * fix(secrets): mark _APPLIED_HOMES only after a real fetch attempt (#40597) (#69056) _apply_external_secret_sources() added the home to _APPLIED_HOMES before loading config, so a malformed config.yaml, a missing secrets section, or all-sources-disabled permanently disabled secret loading for the process — even after the user fixed the config. Long-lived processes (gateway) never recovered without a restart. Now the home is marked only after apply_all() actually ran with at least one enabled source. Fetch errors still mark the home (so import-time load_hermes_dotenv() calls don't re-fetch and re-print the same failure 3-5x per startup); the cheap early-exit paths stay retryable. Fixes #40597. * fix(secrets): fall back to os.environ on scope miss when multiplexing is off fdab380a1 wraps every cron job in a <home>/.env secret scope regardless of deployment mode. get_secret() treats any installed scope as authoritative, so in single-profile deployments where provider keys live only in the process environment (systemd Environment=, pass-cli/op run wrappers, shell exports) every cron credential read returns empty, the OpenAI client is built with the no-key-required placeholder, and each scheduled job 401s — while interactive turns keep working. Scope-miss reads now fall through to os.environ when multiplexing is off; multiplexed scopes stay authoritative. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(gateway): activate multiplex flag in cross-profile env isolation test The test installs a secret scope and asserts a scope miss does NOT fall back to the default profile's env — that isolation guarantee only holds under multiplexing, which the real gateway activates at startup via set_multiplex_active(). With the #67827 overlay fallthrough (scope miss → os.environ when multiplex is OFF), the test needs to model the multiplexed runtime it is actually testing. * feat(secrets): orchestrator-level preserve_existing + profile aliasing (#69058) Fixes the profile-clobber bug cluster at the apply_all() chokepoint so every secret source — bundled and plugin — gets both behaviors for free: - secrets.preserve_existing (#58073): env var names whose existing .env / shell value always wins, even against a source with override_existing: true. Escape hatch for per-profile platform secrets while everything else rotates centrally. - Profile aliasing (#51447): under a named profile, an applied FOO_<PROFILE> var (credential-shaped suffixes only) also hydrates the canonical FOO, so adapters/plugins that read fixed env names see the profile's value. Direct supply beats alias; protected/claimed/ override guards all apply; secrets.profile_alias: false disables. Reimplements the intent of PR #58085 (tianma-if, preserve_existing on the legacy Bitwarden apply shim) and PR #51616 (LeonSGP43, profile aliasing inside the Bitwarden backend) on the SecretSource orchestrator that superseded those code paths. Fixes #58073. Fixes #51447. Co-authored-by: tianma-if <5895871+tianma-if@users.noreply.github.com> Co-authored-by: LeonSGP43 <154585401+LeonSGP43@users.noreply.github.com> * fix(gateway): preserve shared route transport adapter * test(gateway): cover routed transport delivery * fix(secrets): scope BWS-injected provider keys Snapshot values applied by external secret sources per resolved HERMES_HOME so a later profile cannot replace an earlier profile scope through shared os.environ. Keep provider and credential-pool fallback reads on the active secret scope, and fail closed on unscoped multiplex reads. Tests: scripts/run_tests.sh tests/test_env_loader_secret_sources.py tests/test_env_loader_op_bootstrap.py tests/agent/test_secret_scope.py tests/agent/test_credential_pool.py tests/tools/test_credential_pool_env_fallback.py tests/hermes_cli/test_xiaomi_provider.py tests/cron/test_run_one_job.py tests/hermes_cli/test_api_key_providers.py tests/gateway/test_multiplex_credential_isolation.py -q (395 passed) * test(secrets): match real ApplyReport shape in isolation test The fake apply_all in test_external_secret_values_are_isolated_between_homes returned an ApplyReport with no SourceReports; since #69056 the env_loader marks _APPLIED_HOMES (and records snapshots) only when at least one enabled source actually reported, so the fake must include a SourceReport like the real orchestrator always does. * feat(nous): send top-level session_id for provider sticky routing (#69253) * feat(nous): send top-level session_id for provider sticky routing The Nous Portal profile only embedded the session id inside portal tags, so Claude traffic through the portal had no sticky-routing key. Multi-turn sessions could reroute between upstream endpoints (Anthropic/Vertex/ Bedrock), cold-writing a fresh prompt cache on every reroute since each provider's cache is instance-local. Mirror the OpenRouter profile: emit extra_body.session_id whenever the agent has one, pinning every turn of a session to the same endpoint so explicit cache_control breakpoints stay warm. * test: expect top-level session_id in Nous max-iterations summary body Sibling site of the profile change — the max-iterations summary path builds its request through the same NousProfile.build_extra_body(), so its exact-shape assertion now includes the sticky-routing session_id when the agent has a session. * feat(secrets): add `command` secret source + unified secrets.provider selector Brings the agent's secret-source system to parity with the desktop app's `command` secrets provider (hermes-desktop src/main/secrets/commandProvider.ts), so a vault helper configured for the desktop also resolves on the gateway/CLI. NEW agent/secret_sources/command.py — ports the TS provider's security model: - Runs a user-configured helper via `/bin/sh -c`; the requested key travels ONLY in the HERMES_SECRET_KEY env var, never interpolated into the command string, so a hostile key name is inert data (not code). - parse_secret_output mirrors the TS parser: exact dotenv-key match wins; >=2 env-shaped lines without the wanted key -> None; otherwise a bare value; base64 '='-padding disambiguation; cross-key misroute guard (a single OTHER_KEY=realvalue line never leaks into a different wanted key). - Hard 3s timeout (kills the whole process group via killpg, so a forking helper can't keep the pipe open), 1 MiB output cap, POSIX-only (Windows degrades to an empty result + warning). Every failure degrades to "no value"; it never raises and never blocks startup. - Logs ONLY structured fields (code=/signal=/errno=) to stderr; the helper's stderr is piped and DISCARDED; the command string and secret values are never logged. Reuses bitwarden.py's FetchResult so env_loader consumes both sources identically. hermes_cli/env_loader.py — _apply_external_secret_sources now reads a unified `secrets.provider` selector ("env" | "command" | "bitwarden"): - provider=command routes to apply_command_secrets, records the provenance as "command" in _SECRET_SOURCES (so format_secret_source_suffix labels keys "(from command)" — already generic, not duplicated), and re-runs the ASCII credential sanitizer like the bitwarden path. - provider=bitwarden keeps the existing behavior byte-for-byte. - env / unset is a no-op (today's default — zero change for existing users). - BACK-COMPAT: a config with only `secrets.bitwarden.enabled: true` and no `provider` key is treated as provider=bitwarden, so existing Bitwarden users are unaffected. Config (the provider selector, command path, timeouts) lives in config.yaml under `secrets:` per the project rubric — only resolved secret VALUES touch env. Tests: NEW tests/test_command_secret_source.py — 27 cases, E2E against a real temp HERMES_HOME with real chmod+x shell helpers (not mocks): bare/dotenv/ base64 round-trip, cross-key misroute, injection-inert key (canary not created), timeout kill within bound, non-zero-exit degrade, no-secret-in-logs, precedence/override, dispatch via config.yaml provider:command, idempotency, and back-compat bitwarden routing. 27 new + 50 baseline green; wider secrets/env_loader/config surface 229 passed / 5 skipped, no regression. * feat(secrets): rework command source as a registered SecretSource — no provider selector Reworks the salvaged command module into a CommandSource(SecretSource) registered as the third bundled source, composing with Bitwarden and 1Password through the apply_all() orchestrator — enable any combination simultaneously. The original PR's secrets.provider single-selector is deliberately dropped: multi-source is first-class and a mutually exclusive provider switch would regress that. - fetch() only fetches; precedence/override/conflicts/environ writes stay in the orchestrator. ErrorKind classification + remediation hints. - apply_command_secrets() kept as a legacy shim (parser/security helpers unchanged: HERMES_SECRET_KEY data-only key passing, cross-key misroute guard, base64-padding disambiguation, timeout + output cap, structured- fields-only failure logging, stderr discarded). - Dispatch tests rewritten for the registry path incl. an explicit two-sources-compose test; selector tests removed with the selector. - cli-config.yaml.example + docs page (command.md), secrets index entry. - contributors mapping for mvalentin@valensys.net -> 0xr00tf3rr3t. * chore: suppress windows-footgun on the POSIX-gated killpg call _run_helper early-returns on Windows before spawning, so the process- group kill in the timeout path can never execute there. * feat(config): resolve ${env:VAR} SecretRefs in config.yaml, matching MCP config (#69267) MCP server config already resolves Cursor-style ${env:VAR} references (mcp_tool._env_ref_name); config.yaml's expander treated the same shape as a literal string — a confusing half-support. _expand_env_vars() now strips the env: prefix and resolves identically, _env_ref_snapshot() tracks the ref under the REAL var name (preserving the #58514 cache- invalidation contract), and refs with a non-env source prefix (bitwarden:/vault:/file:) warn with a pointer to the secrets: block instead of being silently treated as a variable named 'bitwarden:FOO'. Salvaged from PR #59516 — the audit-CLI half and the main() exit-code change were out of scope and are not included. Co-authored-by: andynguyendk <35395190+andynguyendk@users.noreply.github.com> * fix(secrets): add encrypted Bitwarden stale cache * fix(secrets): harden encrypted Bitwarden cache * fix(secrets): unify encrypted-cache fallback with the merged stale-cache path Rework the encrypted cache onto the fallback that landed in #69051: one transport-only gate, encrypted tier replaces (never accompanies) the plaintext tier when enabled, warning carries the failure + cache age, in-process cache promoted on a stale hit, and clear_caches() (token rotation) also removes the encrypted file since its key derives from the rotated token. * perf(state): external-content FTS + tool-row-free trigram index (schema v23) (#65798) * fix(desktop): refresh repo status on session switch with unchanged cwd (#68208) fix(desktop): refresh repo status on session switch with unchanged cwd * fix(checkpoints): honor gateway config and task cwd (#68195) * fix(gateway): wire checkpoint config into agents * fix(checkpoints): resolve gateway file paths by task cwd * ci: live-updating PR review comment with structured job statuses Replace the static comment-pending + comment-results two-job pattern with a live-updating comment system that polls the GitHub Actions API every 15s, re-assembles the review comment from whatever results are available, and upserts it via the <!-- hermes-ci-review-bot --> marker. The comment updates in real time as each job finishes — no waiting for the full pipeline. Every CI job that wants to appear in the review comment emits a review_status output — a JSON array of objects, each with a source and a results array: [ { "source": "review-label-gate", "results": [ {"kind": "action_required", "title": "...", "summary": "...", "how_to_fix": "..."}, …
1 task
1 task
76 tasks
gabrielcosi
pushed a commit
to gabrielcosi/home-ops
that referenced
this pull request
Aug 5, 2026
….7.30 ➔ v2026.8.3) (#253)
This PR contains the following updates:
| Package | Update | Change |
|---|---|---|
| [ghcr.io/gabrielcosi/hermes-agent](https://github.com/NousResearch/hermes-agent) | minor | `v2026.7.30` → `v2026.8.3` |
---
### Release Notes
<details>
<summary>NousResearch/hermes-agent (ghcr.io/gabrielcosi/hermes-agent)</summary>
### [`v2026.8.3`](https://github.com/NousResearch/hermes-agent/releases/tag/v2026.8.3): Hermes Agent v0.20.0 (2026.8.3)
[Compare Source](https://github.com/NousResearch/hermes-agent/compare/v2026.7.30...v2026.8.3)
##### Hermes Agent v0.20.0 (v2026.8.3)
**Release Date:** August 3, 2026
**Since v0.19.0:** \~3,650 commits · \~1,400 merged PRs · \~5,200 files changed · \~559,000 insertions · \~405,000 deletions · **\~1,200 issues closed** · 650+ contributors
> **The Herald Release.** Hermes is the herald of the gods, and this release makes him one in earnest: he **speaks** (real-time conversational voice with streaming TTS, barge-in, on-device wake words, and hands-free control across the CLI, desktop, and every audio-capable gateway platform), he **carries word to other agents** (A2A v1.0), he **announces events to your systems** (signed outbound webhooks), and he **cites his sources** (grounded research with verifiable citations and fact-checking). Around that spine: the desktop app became a platform (artifacts with live preview, a plugin SDK, quick-entry from anywhere, multiple windows), the CLI got a wave of power commands (`!` shell mode, `/init`, `/diff`, `/context`, `/focus`), compression got smarter and gentler, and the tools themselves now recover from their own failures instead of making the model guess. This release rolls up everything from the v0.19.1 infrastructure patch tag — that window is fully documented here.
***
##### ✨ Highlights
- **Talk to Hermes — streaming, conversational voice with barge-in** — Voice mode used to mean: speak, wait for the whole reply to generate, then listen to one long audio file. Now Hermes speaks clause-by-clause as the response streams, you can interrupt it mid-sentence by just talking (it stops, listens, and the model is told you cut in), and busy-aware silence detection means it doesn't talk over you. This works in CLI voice mode, on the desktop, and through gateway adapters. Talking to Hermes finally feels like a conversation, not a voicemail exchange. ([#​69511](https://github.com/NousResearch/hermes-agent/pull/69511), [#​73862](https://github.com/NousResearch/hermes-agent/pull/73862), [#​74223](https://github.com/NousResearch/hermes-agent/pull/74223), [#​74000](https://github.com/NousResearch/hermes-agent/pull/74000), [#​69602](https://github.com/NousResearch/hermes-agent/pull/69602) — [@​teknium1](https://github.com/teknium1), [@​OutThisLife](https://github.com/OutThisLife))
- **Wake words and hands-free control** — Say your own open-vocabulary wake phrase ("hey Hermes", or anything you pick) and Hermes starts listening — detection runs on-device, so no audio leaves your machine while it waits. Multi-profile voice routing means different wake words can reach different profiles, and saying "stop" ends the voice chat on every surface without touching the keyboard. Your terminal is now something you can talk to from across the room. ([#​70509](https://github.com/NousResearch/hermes-agent/pull/70509), [#​73106](https://github.com/NousResearch/hermes-agent/pull/73106), [#​73933](https://github.com/NousResearch/hermes-agent/pull/73933) — [@​teknium1](https://github.com/teknium1))
- **Voice on every platform** — Send a voice note to Hermes on WhatsApp, Feishu, DingTalk, LINE, QQ, Photon, or Weixin and it's transcribed and answered; auto-TTS replies are delivered platform-aware (opus where platforms want opus, captions attached correctly). STT is now fully configurable — its own `hermes tools` category, GUI toggles, dashboard dropdowns, unified language resolution so transcripts stop coming back in the wrong language, and OpenAI's gpt-transcribe support. One unified spoken-text preprocessor cleans markdown, code, and URLs out of speech across all TTS providers. ([#​73515](https://github.com/NousResearch/hermes-agent/pull/73515), [#​73508](https://github.com/NousResearch/hermes-agent/pull/73508), [#​73910](https://github.com/NousResearch/hermes-agent/pull/73910), [#​73513](https://github.com/NousResearch/hermes-agent/pull/73513), [#​73067](https://github.com/NousResearch/hermes-agent/pull/73067) — [@​teknium1](https://github.com/teknium1))
- **Research you can trust — grounded citations with fact-checking** — The new `grounded-citations` skill makes Hermes produce research where every claim is backed by a verifiable source: quotes are matched against the actual page text (not hallucinated), citations link to the exact evidence, and a fact-checking mode turns the same machinery on any document or claim you hand it — it tells you what checks out, what doesn't, and what couldn't be verified. If you use Hermes for research, this is the difference between "sounds right" and "provably sourced." ([#​71698](https://github.com/NousResearch/hermes-agent/pull/71698), [#​77104](https://github.com/NousResearch/hermes-agent/pull/77104) — [@​teknium1](https://github.com/teknium1))
- **Outbound webhooks — Hermes pushes events to your systems** — Until now, integrating with Hermes meant polling or listening on a platform. Now Hermes pushes **signed lifecycle events** (session activity, turn completions, tool events) to any HTTP endpoint you register — with HMAC signatures so your receiver can verify authenticity. Wire Hermes into your CI, your home automation, your dashboards, or any service that speaks HTTP, with no polling loop. ([#​69406](https://github.com/NousResearch/hermes-agent/pull/69406) — [@​teknium1](https://github.com/teknium1))
- **The desktop app becomes a platform — artifacts, plugin SDK, quick entry** — Hermes desktop now renders **artifacts**: versioned cards with sandboxed live preview in a right-rail viewer, so generated HTML/apps run safely next to the chat. A real **plugin SDK** landed with Kanban as its founding plugin, `ctx.download` for handing users files, floating pane placement, and multiple GUI windows. A global-hotkey **quick-entry window** captures a thought into any session from anywhere in your OS. The desktop stopped being a chat client and started being a workbench. ([#​72345](https://github.com/NousResearch/hermes-agent/pull/72345), [#​61173](https://github.com/NousResearch/hermes-agent/pull/61173), [#​74413](https://github.com/NousResearch/hermes-agent/pull/74413), [#​72315](https://github.com/NousResearch/hermes-agent/pull/72315), [#​68259](https://github.com/NousResearch/hermes-agent/pull/68259), [#​73143](https://github.com/NousResearch/hermes-agent/pull/73143) — [@​OutThisLife](https://github.com/OutThisLife), [@​teknium1](https://github.com/teknium1))
- **Hermes speaks Agent-to-Agent — A2A v1.0** — A new bundled plugin implements the Agent-to-Agent protocol, so Hermes can discover, talk to, and be driven by other A2A-compatible agents. This closes issue [#​514](https://github.com/NousResearch/hermes-agent/issues/514) — one of the oldest open feature requests in the repo. If you're building multi-agent systems with heterogeneous stacks, Hermes now has a standard wire protocol for joining them. ([#​77109](https://github.com/NousResearch/hermes-agent/pull/77109) — [@​teknium1](https://github.com/teknium1))
- **CLI power-user wave** — `!command` runs a shell command instantly without spending a model turn. `/init` scans your project and generates (or updates) an `AGENTS.md`. `/diff` shows staged/all/session changes from any surface, `/context` breaks down exactly what's filling your context window, `/focus` gives you a reduced-output view with hidden-line recovery, and Ctrl+S stashes a half-written prompt into a browsable panel. Plus `hermes import-agent` migrates your Claude Code or Codex CLI setup into Hermes in one command. ([#​72257](https://github.com/NousResearch/hermes-agent/pull/72257), [#​72178](https://github.com/NousResearch/hermes-agent/pull/72178), [#​72240](https://github.com/NousResearch/hermes-agent/pull/72240), [#​72242](https://github.com/NousResearch/hermes-agent/pull/72242), [#​72302](https://github.com/NousResearch/hermes-agent/pull/72302), [#​72262](https://github.com/NousResearch/hermes-agent/pull/72262), [#​72190](https://github.com/NousResearch/hermes-agent/pull/72190) — [@​teknium1](https://github.com/teknium1), several salvaging long-standing community PRs)
- **Correct the agent mid-turn — redirects** — If Hermes is heading the wrong way, you no longer have to `/stop` and re-explain. Type a correction while it works and the active turn is redirected: work in flight is preserved, the original prompt is kept, and the agent course-corrects with your new guidance. Paired with double-ESC draft discard and a composer undo stack, steering feels like editing, not restarting. ([#​63104](https://github.com/NousResearch/hermes-agent/pull/63104), [#​72339](https://github.com/NousResearch/hermes-agent/pull/72339), [#​74736](https://github.com/NousResearch/hermes-agent/pull/74736) — [@​OutThisLife](https://github.com/OutThisLife))
- **Tools that fix themselves** — A sweep of self-recovery upgrades means the agent wastes far fewer turns on tool friction: truncated terminal output spills to a file the agent can read back, `patch` detects already-applied edits and diagnoses whitespace mismatches, `write_file` verifies content on disk, searches that match nothing probe for near-misses and recover, and common failure classes come back with actionable hints. The default tool-calling iteration limit also jumped 90 → 500 — long autonomous runs stopped hitting an artificial wall. ([#​77041](https://github.com/NousResearch/hermes-agent/pull/77041), [#​76998](https://github.com/NousResearch/hermes-agent/pull/76998), [#​77024](https://github.com/NousResearch/hermes-agent/pull/77024), [#​77055](https://github.com/NousResearch/hermes-agent/pull/77055), [#​77011](https://github.com/NousResearch/hermes-agent/pull/77011), [#​76992](https://github.com/NousResearch/hermes-agent/pull/76992), [#​72176](https://github.com/NousResearch/hermes-agent/pull/72176) — [@​teknium1](https://github.com/teknium1))
- **Compression that respects your conversation** — Context compression got a deep overhaul: proactive tool-result pruning for large-window models, per-turn micro-compaction that amortizes the cost instead of one giant pause, a guaranteed N-user-message tail so recent conversation always survives, progress-aware timeouts that stop punishing slow summary models, and ghost-skill defense so a pruned skill can never silently haunt a session. Thresholds are now configurable per-model and in absolute tokens. Long sessions stay coherent and stop stalling. ([#​70254](https://github.com/NousResearch/hermes-agent/pull/70254), [#​75345](https://github.com/NousResearch/hermes-agent/pull/75345), [#​70250](https://github.com/NousResearch/hermes-agent/pull/70250), [#​71508](https://github.com/NousResearch/hermes-agent/pull/71508), [#​70275](https://github.com/NousResearch/hermes-agent/pull/70275) — [@​teknium1](https://github.com/teknium1), [@​kshitijk4poor](https://github.com/kshitijk4poor), salvaging multiple community PRs)
- **Smart approvals grow up** — `hermes approvals suggest` mines your approval history into allowlist proposals, operators can customize the smart-approval policy, a consecutive-denial circuit breaker stops a misbehaving loop cold, and desktop pairing approvals are profile-correct with a proper surface to answer them from. Plus a new approval gate for docker/podman daemon-redirect commands. Less clicking "approve", without giving an inch of control. ([#​72259](https://github.com/NousResearch/hermes-agent/pull/72259), [#​72186](https://github.com/NousResearch/hermes-agent/pull/72186), [#​72203](https://github.com/NousResearch/hermes-agent/pull/72203), [#​74446](https://github.com/NousResearch/hermes-agent/pull/74446), [#​71092](https://github.com/NousResearch/hermes-agent/pull/71092) — [@​teknium1](https://github.com/teknium1), [@​OutThisLife](https://github.com/OutThisLife))
- **Faster everywhere, again** — Prompt caching now covers tool schemas on native Anthropic without history loss. `hermes -w` cold start dropped \~14s → \~1.8s, `hermes update` no-ops got 2–6s faster, heavy SDKs lazy-load off the import path, config reads stopped deep-copying (54× faster on the telemetry gate), and the desktop shipped a second 60fps wave — streaming cost independent of transcript length, drag at 60fps with five streaming tabs, idle CPU near zero in the background. ([#​76032](https://github.com/NousResearch/hermes-agent/pull/76032), [#​71637](https://github.com/NousResearch/hermes-agent/pull/71637), [#​74218](https://github.com/NousResearch/hermes-agent/pull/74218), [#​74204](https://github.com/NousResearch/hermes-agent/pull/74204), [#​71835](https://github.com/NousResearch/hermes-agent/pull/71835), [#​72346](https://github.com/NousResearch/hermes-agent/pull/72346), [#​75218](https://github.com/NousResearch/hermes-agent/pull/75218) — [@​kshitijk4poor](https://github.com/kshitijk4poor), [@​teknium1](https://github.com/teknium1), [@​OutThisLife](https://github.com/OutThisLife))
- **New places to run and be reached** — Buzz lands as a bundled gateway platform (Block's Nostr-based messenger, with native WebSocket transport and NIP-42 auth), the Vercel AI Gateway provider and Vercel Sandbox terminal backend return modernized, desktop gains an SSH remote-backend connection mode, and the Relay shipped four phases of parity — media, interactive prompts, thread lifecycle, typing indicators — plus HSP personal + org skill sync. ([#​73610](https://github.com/NousResearch/hermes-agent/pull/73610), [#​73761](https://github.com/NousResearch/hermes-agent/pull/73761), [#​74518](https://github.com/NousResearch/hermes-agent/pull/74518), [#​68130](https://github.com/NousResearch/hermes-agent/pull/68130), [#​71300](https://github.com/NousResearch/hermes-agent/pull/71300)–[#​71624](https://github.com/NousResearch/hermes-agent/pull/71624), [#​66730](https://github.com/NousResearch/hermes-agent/pull/66730) — [@​teknium1](https://github.com/teknium1), [@​yoniebans](https://github.com/yoniebans), [@​benbarclay](https://github.com/benbarclay))
***
##### 🎙️ Voice & Speech
##### Conversational voice
- Streaming, conversational TTS with barge-in across all surfaces; clause-by-clause synthesis for CLI voice mode + gateway adapters ([#​69511](https://github.com/NousResearch/hermes-agent/pull/69511), [#​73862](https://github.com/NousResearch/hermes-agent/pull/73862) — [@​OutThisLife](https://github.com/OutThisLife), [@​teknium1](https://github.com/teknium1))
- Voice chat UX polish — busy-aware silence, stop hint, thinking sounds, barge-in fix; full-duplex turn listener (interrupt by voice during generation AND playback) ([#​74000](https://github.com/NousResearch/hermes-agent/pull/74000), [#​74223](https://github.com/NousResearch/hermes-agent/pull/74223) — [@​teknium1](https://github.com/teknium1))
- On-device wake words with open-vocabulary phrases + multi-profile voice routing; say "stop" to end voice chat hands-free on every surface ([#​70509](https://github.com/NousResearch/hermes-agent/pull/70509), [#​73106](https://github.com/NousResearch/hermes-agent/pull/73106), [#​73933](https://github.com/NousResearch/hermes-agent/pull/73933) — [@​teknium1](https://github.com/teknium1))
- The model is told when the user interrupts its spoken reply; desktop speaks the whole turn and idle-flushes held narration ([#​69602](https://github.com/NousResearch/hermes-agent/pull/69602), [#​69936](https://github.com/NousResearch/hermes-agent/pull/69936) — [@​OutThisLife](https://github.com/OutThisLife), [@​teknium1](https://github.com/teknium1))
- 15-item CLI/TUI voice-mode UX and environment fix wave ([#​73520](https://github.com/NousResearch/hermes-agent/pull/73520) — [@​teknium1](https://github.com/teknium1))
##### TTS / STT infrastructure
- Unified spoken-text preprocessing + speed/instructions/provider tool params; unified STT language resolution (fixes the wrong-language transcription class); global `stt.language` defaults to `en` ([#​73513](https://github.com/NousResearch/hermes-agent/pull/73513), [#​73067](https://github.com/NousResearch/hermes-agent/pull/73067), [#​73100](https://github.com/NousResearch/hermes-agent/pull/73100) — [@​teknium1](https://github.com/teknium1))
- Fully configurable STT — `hermes tools` category, GUI toggle/matrix, dashboard dropdowns, setup status; OpenAI gpt-transcribe support ([#​73910](https://github.com/NousResearch/hermes-agent/pull/73910), [#​73853](https://github.com/NousResearch/hermes-agent/pull/73853) — [@​teknium1](https://github.com/teknium1))
- Platform-aware auto-TTS voice delivery (opus platforms, streamed/global gap, captions); inbound voice classification/routing for Feishu, DingTalk, LINE, QQ, Photon, WhatsApp, Weixin ([#​73508](https://github.com/NousResearch/hermes-agent/pull/73508), [#​73515](https://github.com/NousResearch/hermes-agent/pull/73515) — [@​teknium1](https://github.com/teknium1))
- Command TTS/STT provider hardening — idle timeouts, env scrubbing, no-shell, path guards ([#​73514](https://github.com/NousResearch/hermes-agent/pull/73514) — [@​teknium1](https://github.com/teknium1))
- Sync per-sentence TTS synthesis pipelined with playback — the next sentence renders while the current one speaks ([#​77355](https://github.com/NousResearch/hermes-agent/pull/77355) — [@​kshitijk4poor](https://github.com/kshitijk4poor))
- Discord voice PCM streams to ffmpeg stdin instead of a temp file ([#​76970](https://github.com/NousResearch/hermes-agent/pull/76970) — [@​kshitijk4poor](https://github.com/kshitijk4poor))
##### 🏗️ Core Agent & Architecture
##### Compression & context
- Proactive tool-result pruning for large-window models; per-turn micro-compaction; N-user tail guarantee (`compression.min_tail_user_messages`); bounded summarizer input with head+tail retention ([#​70254](https://github.com/NousResearch/hermes-agent/pull/70254), [#​75345](https://github.com/NousResearch/hermes-agent/pull/75345), [#​70250](https://github.com/NousResearch/hermes-agent/pull/70250), [#​70249](https://github.com/NousResearch/hermes-agent/pull/70249) — [@​teknium1](https://github.com/teknium1), [@​kshitijk4poor](https://github.com/kshitijk4poor))
- Ghost-skill defense — `[SKILL_PRUNED]` markers, protected prune, deterministic survival; progress-aware timeouts; lock-contended compression soft-defers instead of exhausting ([#​70275](https://github.com/NousResearch/hermes-agent/pull/70275), [#​71508](https://github.com/NousResearch/hermes-agent/pull/71508), [#​70285](https://github.com/NousResearch/hermes-agent/pull/70285) — [@​teknium1](https://github.com/teknium1))
- Per-model threshold overrides; absolute token threshold (`compression.threshold_tokens`); opt-in idle-triggered compaction; opt-in progress notices; structured local logging for compression attempts ([#​69339](https://github.com/NousResearch/hermes-agent/pull/69339), [#​69335](https://github.com/NousResearch/hermes-agent/pull/69335), [#​69360](https://github.com/NousResearch/hermes-agent/pull/69360), [#​70457](https://github.com/NousResearch/hermes-agent/pull/70457), [#​69338](https://github.com/NousResearch/hermes-agent/pull/69338) — [@​teknium1](https://github.com/teknium1))
- Context-engine ABC grows `select_context()` + `on_turn_complete()` verbs (salvage of [@​chaos-xxl](https://github.com/chaos-xxl)'s RFC work); engines can suppress or customize compaction status ([#​70458](https://github.com/NousResearch/hermes-agent/pull/70458), [#​69859](https://github.com/NousResearch/hermes-agent/pull/69859) — [@​teknium1](https://github.com/teknium1))
- Strict redaction applied at every compaction text boundary ([#​69294](https://github.com/NousResearch/hermes-agent/pull/69294) — [@​teknium1](https://github.com/teknium1))
##### Prompt caching & hot-path performance
- Tool schemas cached on native Anthropic without history loss + consolidated cache-plan internals ([#​76032](https://github.com/NousResearch/hermes-agent/pull/76032), [#​76067](https://github.com/NousResearch/hermes-agent/pull/76067) — [@​kshitijk4poor](https://github.com/kshitijk4poor))
- DeepSeek prompt caching on OpenCode gateways; per-API-call token accounting off the turn thread; OpenAI wire client reused across sequential LLM calls; send-path tool-call canonicalization memoized ([#​75886](https://github.com/NousResearch/hermes-agent/pull/75886), [#​73359](https://github.com/NousResearch/hermes-agent/pull/73359), [#​73375](https://github.com/NousResearch/hermes-agent/pull/73375), [#​76880](https://github.com/NousResearch/hermes-agent/pull/76880) — [@​teknium1](https://github.com/teknium1), [@​kshitijk4poor](https://github.com/kshitijk4poor))
- Readonly config loader at 29 call sites (28× cheaper reads); per-turn config deepcopies killed (telemetry gate 54×); one raw config.yaml parse per process; inter-tool delay removed ([#​74322](https://github.com/NousResearch/hermes-agent/pull/74322), [#​74211](https://github.com/NousResearch/hermes-agent/pull/74211), [#​74228](https://github.com/NousResearch/hermes-agent/pull/74228), [#​64172](https://github.com/NousResearch/hermes-agent/pull/64172) — [@​teknium1](https://github.com/teknium1), [@​Soju06](https://github.com/Soju06))
- Lazy heavy-SDK imports (−8-10% import cost on top of the mcp/tool-discovery diet); streaming hot loop drops per-chunk repr() (\~3× cheaper accounting); cursor/memo optimizations for per-iteration history walks ([#​74204](https://github.com/NousResearch/hermes-agent/pull/74204), [#​74194](https://github.com/NousResearch/hermes-agent/pull/74194), [#​74221](https://github.com/NousResearch/hermes-agent/pull/74221), [#​74231](https://github.com/NousResearch/hermes-agent/pull/74231) — [@​teknium1](https://github.com/teknium1))
- Cold-start \~14s GIL stall during backend init mitigated; turn flush batched into one SQLite transaction; provider-capability-gated prompt cache keys (implied for api.openai.com) ([#​77814](https://github.com/NousResearch/hermes-agent/pull/77814), [#​77619](https://github.com/NousResearch/hermes-agent/pull/77619), [#​77609](https://github.com/NousResearch/hermes-agent/pull/77609) — [@​kshitijk4poor](https://github.com/kshitijk4poor))
- AIAgent hot-path salvage — prompt-cache copy, reasoning-timeout precompute, lazy compressor init ([#​57229](https://github.com/NousResearch/hermes-agent/pull/57229) — [@​kshitijk4poor](https://github.com/kshitijk4poor))
##### Approvals & the agent loop
- `hermes approvals suggest` mines approval history into allowlist proposals; operator-customizable `approvals.smart_policy`; consecutive-denial circuit breaker; cross-surface approvals mode command ([#​72259](https://github.com/NousResearch/hermes-agent/pull/72259), [#​72186](https://github.com/NousResearch/hermes-agent/pull/72186), [#​72203](https://github.com/NousResearch/hermes-agent/pull/72203), [#​63517](https://github.com/NousResearch/hermes-agent/pull/63517) — [@​teknium1](https://github.com/teknium1))
- Docker/podman daemon-redirect commands require approval; session-wide runaway-loop caps for web\_search + delegate\_task (Claude Code-inspired) ([#​71092](https://github.com/NousResearch/hermes-agent/pull/71092), [#​66600](https://github.com/NousResearch/hermes-agent/pull/66600) — [@​teknium1](https://github.com/teknium1))
- Mid-turn redirects — user corrections steer the active turn, preserving in-flight work and the original prompt ([#​63104](https://github.com/NousResearch/hermes-agent/pull/63104), [#​72339](https://github.com/NousResearch/hermes-agent/pull/72339) — [@​OutThisLife](https://github.com/OutThisLife))
- Delegation: structured timeout/stall metadata + live per-child status in `/agents`; subagents can use `execute_code`; redacted child tool history exposed in `subagent_stop`; public subagent lifecycle API for plugins ([#​72300](https://github.com/NousResearch/hermes-agent/pull/72300), [#​69325](https://github.com/NousResearch/hermes-agent/pull/69325), [#​72403](https://github.com/NousResearch/hermes-agent/pull/72403), [#​72501](https://github.com/NousResearch/hermes-agent/pull/72501) — [@​teknium1](https://github.com/teknium1))
- Single-owner refactors for backend identity + failure-scoped skips, empty-content wire repair, call\_id/reasoning sanitization, model-switch parsing ([#​72505](https://github.com/NousResearch/hermes-agent/pull/72505), [#​73071](https://github.com/NousResearch/hermes-agent/pull/73071), [#​74319](https://github.com/NousResearch/hermes-agent/pull/74319), [#​74229](https://github.com/NousResearch/hermes-agent/pull/74229) — [@​teknium1](https://github.com/teknium1))
- Labeled reasoning excerpt surfaced at the empty-response terminal; tool\_search probe-validates blind tool\_call args ([#​65144](https://github.com/NousResearch/hermes-agent/pull/65144), [#​59267](https://github.com/NousResearch/hermes-agent/pull/59267) — [@​teknium1](https://github.com/teknium1))
##### Tool self-recovery wave
- Terminal: recoverable truncation (full output spilled + pre-truncation size), cwd echoed when a command changes directory, output-pattern failure hints ([#​77041](https://github.com/NousResearch/hermes-agent/pull/77041), [#​77004](https://github.com/NousResearch/hermes-agent/pull/77004), [#​76992](https://github.com/NousResearch/hermes-agent/pull/76992) — [@​teknium1](https://github.com/teknium1))
- Patch: already-applied edits return success no-op, whitespace-visualized no-match diagnosis, ambiguous-match locations listed ([#​76998](https://github.com/NousResearch/hermes-agent/pull/76998), [#​77024](https://github.com/NousResearch/hermes-agent/pull/77024), [#​77001](https://github.com/NousResearch/hermes-agent/pull/77001) — [@​teknium1](https://github.com/teknium1))
- Search: zero-match probes + multi-path recovery, auto-multiline for newline patterns; read\_file default limit 500 → 2000 lines; negative-result cache for read/search misses; write\_file verifies on-disk content ([#​77011](https://github.com/NousResearch/hermes-agent/pull/77011), [#​77102](https://github.com/NousResearch/hermes-agent/pull/77102), [#​76996](https://github.com/NousResearch/hermes-agent/pull/76996), [#​76945](https://github.com/NousResearch/hermes-agent/pull/76945), [#​77055](https://github.com/NousResearch/hermes-agent/pull/77055) — [@​teknium1](https://github.com/teknium1), [@​kshitijk4poor](https://github.com/kshitijk4poor))
- execute\_code recovery hints; skill\_view dedup stub for unchanged re-reads; terminal/execute\_code schema prose trimmed \~40%; tiered tool disclosure scales with catalog size; default iteration limit 90 → 500 ([#​77106](https://github.com/NousResearch/hermes-agent/pull/77106), [#​77095](https://github.com/NousResearch/hermes-agent/pull/77095), [#​77023](https://github.com/NousResearch/hermes-agent/pull/77023), [#​67034](https://github.com/NousResearch/hermes-agent/pull/67034), [#​72176](https://github.com/NousResearch/hermes-agent/pull/72176) — [@​teknium1](https://github.com/teknium1))
##### Providers & models
- Vercel AI Gateway provider + Vercel Sandbox terminal backend return, modernized (SDK 0.7.2, telemetry off) ([#​74518](https://github.com/NousResearch/hermes-agent/pull/74518) — [@​teknium1](https://github.com/teknium1))
- Gemini 3.1 Pro + 3.6 Flash in catalogs; Gemini salvage cluster (3.6-flash aux default, Vertex catalog, direct cost tracking); claude-opus-5 in OpenRouter + Nous Portal; deepseek-v4-flash-0731 ([#​73479](https://github.com/NousResearch/hermes-agent/pull/73479), [#​73516](https://github.com/NousResearch/hermes-agent/pull/73516), [#​70946](https://github.com/NousResearch/hermes-agent/pull/70946), [#​75501](https://github.com/NousResearch/hermes-agent/pull/75501) — [@​teknium1](https://github.com/teknium1))
- Bedrock Converse API prompt caching (cachePoint) ([#​70231](https://github.com/NousResearch/hermes-agent/pull/70231) — [@​JoaoMarcos44](https://github.com/JoaoMarcos44))
- OpenAI data-residency endpoints get declared transport + correct catalog; provider-aware API-server request routing; backend-acknowledged session model lock; Nous sticky routing via top-level session\_id ([#​74958](https://github.com/NousResearch/hermes-agent/pull/74958), [#​70853](https://github.com/NousResearch/hermes-agent/pull/70853), [#​70950](https://github.com/NousResearch/hermes-agent/pull/70950), [#​69253](https://github.com/NousResearch/hermes-agent/pull/69253) — [@​victor-kyriazakos](https://github.com/victor-kyriazakos), [@​teknium1](https://github.com/teknium1))
- Model picker: curated defaults + collapsible providers + select-all; stale caches served instantly with background refresh; custom-endpoint probe capped at 1.5s; honcho OAuth device-code login ([#​73172](https://github.com/NousResearch/hermes-agent/pull/73172), [#​76430](https://github.com/NousResearch/hermes-agent/pull/76430), [#​76922](https://github.com/NousResearch/hermes-agent/pull/76922), [#​61608](https://github.com/NousResearch/hermes-agent/pull/61608) — [@​OutThisLife](https://github.com/OutThisLife), [@​teknium1](https://github.com/teknium1), [@​kshitijk4poor](https://github.com/kshitijk4poor), [@​akattelu](https://github.com/akattelu))
- ACP: named custom providers in the model selector; authenticated cross-provider model choices; non-blocking startup via background MCP discovery ([#​70082](https://github.com/NousResearch/hermes-agent/pull/70082), [#​70404](https://github.com/NousResearch/hermes-agent/pull/70404), [#​75985](https://github.com/NousResearch/hermes-agent/pull/75985) — [@​israellot](https://github.com/israellot), [@​amanning3390](https://github.com/amanning3390), [@​kshitijk4poor](https://github.com/kshitijk4poor))
##### Secrets & config
- Command-helper secret source (composes with all vaults); one-command token rotation + actionable startup errors; opt-in encrypted break-glass cache for Bitwarden; vault-injected keys scoped per profile home; orchestrator preserve\_existing + profile aliasing ([#​69266](https://github.com/NousResearch/hermes-agent/pull/69266), [#​68605](https://github.com/NousResearch/hermes-agent/pull/68605), [#​69251](https://github.com/NousResearch/hermes-agent/pull/69251), [#​69250](https://github.com/NousResearch/hermes-agent/pull/69250), [#​69058](https://github.com/NousResearch/hermes-agent/pull/69058) — [@​teknium1](https://github.com/teknium1))
- `${env:VAR}` SecretRef parity between config.yaml and MCP config; secret-source env vars reach stdio MCP servers ([#​69267](https://github.com/NousResearch/hermes-agent/pull/69267), [#​69053](https://github.com/NousResearch/hermes-agent/pull/69053) — [@​teknium1](https://github.com/teknium1))
- Canonical config loaders for behavioral reads; table-driven config migration registry; DEFAULT\_CONFIG extracted to config\_defaults.py; auto-migration support floor at v12 ([#​74237](https://github.com/NousResearch/hermes-agent/pull/74237), [#​74200](https://github.com/NousResearch/hermes-agent/pull/74200), [#​74182](https://github.com/NousResearch/hermes-agent/pull/74182), [#​74433](https://github.com/NousResearch/hermes-agent/pull/74433) — [@​teknium1](https://github.com/teknium1))
##### 🌐 Gateway, Relay & Fleet
- Session activity heartbeats, stall watchdog, and bounded compression waits — re-landed hardened after an in-window revert cycle (originally [#​72424](https://github.com/NousResearch/hermes-agent/issues/72424) by [@​fangliquanflq](https://github.com/fangliquanflq)) ([#​76354](https://github.com/NousResearch/hermes-agent/pull/76354) — [@​teknium1](https://github.com/teknium1))
- SessionState consolidation (19 session-keyed dicts → one turn/conversation/persistent-scoped object); TurnContext/TurnRunner seam extraction; declarative busy\_policy on CommandDef ([#​74289](https://github.com/NousResearch/hermes-agent/pull/74289), [#​74353](https://github.com/NousResearch/hermes-agent/pull/74353), [#​74197](https://github.com/NousResearch/hermes-agent/pull/74197) — [@​teknium1](https://github.com/teknium1))
- Relay parity waves: Phase 1 (supported\_ops discovery, identity fields, /handoff aliasing), Phase 2 media, Phase 3 interactive prompts, Phase 4 thread lifecycle; egress typing indicators ([#​71300](https://github.com/NousResearch/hermes-agent/pull/71300), [#​71363](https://github.com/NousResearch/hermes-agent/pull/71363), [#​71404](https://github.com/NousResearch/hermes-agent/pull/71404), [#​71624](https://github.com/NousResearch/hermes-agent/pull/71624), [#​69721](https://github.com/NousResearch/hermes-agent/pull/69721) — [@​benbarclay](https://github.com/benbarclay))
- HSP skill sync: personal client (M1) + org-skills client (M2) + org-skill namespace with token-gated discovery ([#​66730](https://github.com/NousResearch/hermes-agent/pull/66730), [#​70024](https://github.com/NousResearch/hermes-agent/pull/70024), [#​70459](https://github.com/NousResearch/hermes-agent/pull/70459) — [@​benbarclay](https://github.com/benbarclay))
- Buzz (Block/Nostr) platform adapter with native WebSocket inbound transport + NIP-42 auth ([#​73610](https://github.com/NousResearch/hermes-agent/pull/73610), [#​73761](https://github.com/NousResearch/hermes-agent/pull/73761) — [@​teknium1](https://github.com/teknium1))
- Photon: native polls, effects, clarify-as-poll, rich links (4-PR salvage) ([#​73614](https://github.com/NousResearch/hermes-agent/pull/73614) — [@​teknium1](https://github.com/teknium1))
- Slack: native Block Kit clarify buttons; opt-in reaction triggers; outbound payload sanitization; thread-context lifecycle fixes ([#​69318](https://github.com/NousResearch/hermes-agent/pull/69318), [#​70195](https://github.com/NousResearch/hermes-agent/pull/70195), [#​69317](https://github.com/NousResearch/hermes-agent/pull/69317), [#​69320](https://github.com/NousResearch/hermes-agent/pull/69320) — [@​teknium1](https://github.com/teknium1))
- Discord auto-thread sessions keyed on prospective\_thread\_id; reply references built from ids (no fetch\_message); WhatsApp configurable inbound read receipts ([#​76513](https://github.com/NousResearch/hermes-agent/pull/76513), [#​76875](https://github.com/NousResearch/hermes-agent/pull/76875), [#​73322](https://github.com/NousResearch/hermes-agent/pull/73322) — [@​benbarclay](https://github.com/benbarclay), [@​kshitijk4poor](https://github.com/kshitijk4poor))
- Kanban wakes resume the creator's DM/thread session; kanban/delegate wake-ups reach api\_server sessions; per-task model + thinking-depth from the board ([#​72191](https://github.com/NousResearch/hermes-agent/pull/72191), [#​70171](https://github.com/NousResearch/hermes-agent/pull/70171), [#​69876](https://github.com/NousResearch/hermes-agent/pull/69876), [#​76417](https://github.com/NousResearch/hermes-agent/pull/76417) — [@​teknium1](https://github.com/teknium1), [@​OutThisLife](https://github.com/OutThisLife))
- Relay: Discord tool-progress routed into the auto-thread instead of the parent channel ([#​77830](https://github.com/NousResearch/hermes-agent/pull/77830) — [@​benbarclay](https://github.com/benbarclay))
- Outbound webhooks — push signed lifecycle events to external endpoints; simplex channel enumeration in `hermes send --list` ([#​69406](https://github.com/NousResearch/hermes-agent/pull/69406), [#​77110](https://github.com/NousResearch/hermes-agent/pull/77110) — [@​teknium1](https://github.com/teknium1))
##### 🖥️ Hermes Desktop App
##### The platform wave
- **Artifacts** — versioned cards, sandboxed live preview, right-rail viewer ([#​72345](https://github.com/NousResearch/hermes-agent/pull/72345) — [@​teknium1](https://github.com/teknium1))
- **Plugin SDK** — Kanban as the founding desktop plugin; `ctx.download` hands the user a file; widget-app SDK (apps as state+reducer+render) with three reference apps; widget-grid layout engine + background-aware theme engine ([#​61173](https://github.com/NousResearch/hermes-agent/pull/61173), [#​74413](https://github.com/NousResearch/hermes-agent/pull/74413), [#​68306](https://github.com/NousResearch/hermes-agent/pull/68306), [#​20379](https://github.com/NousResearch/hermes-agent/pull/20379) — [@​OutThisLife](https://github.com/OutThisLife))
- Quick-entry window (global hotkey → any session); multiple GUI windows; floating pane placement; pane toggles anywhere + hidden header; ⌘O open-folder-as-project ([#​72315](https://github.com/NousResearch/hermes-agent/pull/72315), [#​68259](https://github.com/NousResearch/hermes-agent/pull/68259), [#​73143](https://github.com/NousResearch/hermes-agent/pull/73143), [#​75848](https://github.com/NousResearch/hermes-agent/pull/75848), [#​74623](https://github.com/NousResearch/hermes-agent/pull/74623) — [@​teknium1](https://github.com/teknium1), [@​OutThisLife](https://github.com/OutThisLife))
- SSH remote-backend connection mode; event-driven live sync replaces always-on polls; remote profile routing/sessions/pool lifecycle repaired ([#​68130](https://github.com/NousResearch/hermes-agent/pull/68130), [#​73673](https://github.com/NousResearch/hermes-agent/pull/73673), [#​72835](https://github.com/NousResearch/hermes-agent/pull/72835) — [@​yoniebans](https://github.com/yoniebans), [@​OutThisLife](https://github.com/OutThisLife))
- Let the agent drive the shell (preview pane + pane focus) AND inspect the desktop app it's developing; find-in-page (Ctrl+F); GUI terminal copy/paste + font picker ([#​69519](https://github.com/NousResearch/hermes-agent/pull/69519), [#​73121](https://github.com/NousResearch/hermes-agent/pull/73121), [#​72235](https://github.com/NousResearch/hermes-agent/pull/72235), [#​73705](https://github.com/NousResearch/hermes-agent/pull/73705), [#​76395](https://github.com/NousResearch/hermes-agent/pull/76395) — [@​OutThisLife](https://github.com/OutThisLife), [@​teknium1](https://github.com/teknium1))
##### Composer & UX
- Attach files/folders/links via picker; composer chips for @​ paths and pasted links; composer undo stack; double-ESC discards draft; double-Enter sends the queued turn; type-to-focus ([#​74668](https://github.com/NousResearch/hermes-agent/pull/74668), [#​73110](https://github.com/NousResearch/hermes-agent/pull/73110), [#​72201](https://github.com/NousResearch/hermes-agent/pull/72201), [#​72288](https://github.com/NousResearch/hermes-agent/pull/72288), [#​74736](https://github.com/NousResearch/hermes-agent/pull/74736), [#​73101](https://github.com/NousResearch/hermes-agent/pull/73101), [#​68918](https://github.com/NousResearch/hermes-agent/pull/68918) — [@​OutThisLife](https://github.com/OutThisLife))
- 2-keypress model switching (⌘⇧M); YOLO in ⌘K with live toggle state; keyboard-first pickers; keyboard navigation for clarify choices; server-owned pins that follow you between apps ([#​74545](https://github.com/NousResearch/hermes-agent/pull/74545), [#​74674](https://github.com/NousResearch/hermes-agent/pull/74674), [#​74602](https://github.com/NousResearch/hermes-agent/pull/74602), [#​69799](https://github.com/NousResearch/hermes-agent/pull/69799), [#​74234](https://github.com/NousResearch/hermes-agent/pull/74234) — [@​OutThisLife](https://github.com/OutThisLife))
- Grouped, live-ticking tool-activity line; improved tool call detail views; [@​session](https://github.com/session) links resolve to clickable titles; brand icons on known-domain links; iMessage-style emoji reactions (opt-in, two-way); double-click to heart ([#​72893](https://github.com/NousResearch/hermes-agent/pull/72893), [#​69868](https://github.com/NousResearch/hermes-agent/pull/69868), [#​71162](https://github.com/NousResearch/hermes-agent/pull/71162), [#​73047](https://github.com/NousResearch/hermes-agent/pull/73047), [#​74533](https://github.com/NousResearch/hermes-agent/pull/74533), [#​74644](https://github.com/NousResearch/hermes-agent/pull/74644) — [@​OutThisLife](https://github.com/OutThisLife), [@​teknium1](https://github.com/teknium1))
- Sidebar date dividers + pinned section + opt-in stale-session auto-archive; sessions stop lying about running state; credit-usage toasts; configurable attachment size limit; Cron Blueprints + Webhooks pages; searchable timezone picker ([#​70822](https://github.com/NousResearch/hermes-agent/pull/70822), [#​72303](https://github.com/NousResearch/hermes-agent/pull/72303), [#​69828](https://github.com/NousResearch/hermes-agent/pull/69828), [#​73221](https://github.com/NousResearch/hermes-agent/pull/73221), [#​70066](https://github.com/NousResearch/hermes-agent/pull/70066), [#​69687](https://github.com/NousResearch/hermes-agent/pull/69687), [#​73505](https://github.com/NousResearch/hermes-agent/pull/73505) — [@​OutThisLife](https://github.com/OutThisLife), [@​austinpickett](https://github.com/austinpickett), [@​Adolanium](https://github.com/Adolanium), [@​teknium1](https://github.com/teknium1))
- RFC 8252 native desktop sign-in (system browser + PKCE, no webview cookies); "Connect to existing Hermes" in first-run onboarding; profile-correct pairing approvals with a desktop surface ([#​67920](https://github.com/NousResearch/hermes-agent/pull/67920), [#​70907](https://github.com/NousResearch/hermes-agent/pull/70907), [#​74446](https://github.com/NousResearch/hermes-agent/pull/74446) — [@​benbarclay](https://github.com/benbarclay), [@​OutThisLife](https://github.com/OutThisLife))
- Keep-computer-awake toggle + notch wake indicator; /battery status-bar toggle; UI zoom 90% default preset; status bar hideable ([#​68140](https://github.com/NousResearch/hermes-agent/pull/68140), [#​76396](https://github.com/NousResearch/hermes-agent/pull/76396), [#​68860](https://github.com/NousResearch/hermes-agent/pull/68860), [#​73161](https://github.com/NousResearch/hermes-agent/pull/73161), [#​72960](https://github.com/NousResearch/hermes-agent/pull/72960) — [@​OutThisLife](https://github.com/OutThisLife), [@​teknium1](https://github.com/teknium1))
##### Desktop performance (60fps wave 2)
- Streaming cost independent of transcript length; 60fps on real sessions (reflow-gated pins, adaptive flush); drag at 60fps with five streaming tabs; multitab streaming made fast ([#​71835](https://github.com/NousResearch/hermes-agent/pull/71835), [#​72504](https://github.com/NousResearch/hermes-agent/pull/72504), [#​72346](https://github.com/NousResearch/hermes-agent/pull/72346), [#​71780](https://github.com/NousResearch/hermes-agent/pull/71780) — [@​OutThisLife](https://github.com/OutThisLife))
- Hidden-pane timers paused (agents view, cron sidebar, floating pet), scroll/status loops stopped in busy sessions ([#​77651](https://github.com/NousResearch/hermes-agent/pull/77651) — [@​kshitijk4poor](https://github.com/kshitijk4poor)); idle CPU near zero in the background; sidebar/overlay render churn killed; statusbar + transcript stop re-rendering per token/sash-drag/session-switch; ⌘K opens instantly; renderer cold start keeps shiki/mermaid off the boot path ([#​75218](https://github.com/NousResearch/hermes-agent/pull/75218), [#​73698](https://github.com/NousResearch/hermes-agent/pull/73698), [#​72163](https://github.com/NousResearch/hermes-agent/pull/72163), [#​72245](https://github.com/NousResearch/hermes-agent/pull/72245), [#​72524](https://github.com/NousResearch/hermes-agent/pull/72524), [#​74665](https://github.com/NousResearch/hermes-agent/pull/74665), [#​73024](https://github.com/NousResearch/hermes-agent/pull/73024) — [@​OutThisLife](https://github.com/OutThisLife))
- State diagnostics (render + store churn counters) + a lint rule banning atom-mirrored refs so the stale-read bug class cannot return; Playwright E2E suite with visual regression diffs ([#​71925](https://github.com/NousResearch/hermes-agent/pull/71925), [#​71560](https://github.com/NousResearch/hermes-agent/pull/71560), [#​65805](https://github.com/NousResearch/hermes-agent/pull/65805) — [@​OutThisLife](https://github.com/OutThisLife), [@​teknium1](https://github.com/teknium1), [@​ethernet8023](https://github.com/ethernet8023))
##### 🖥️ CLI, TUI & Dashboard
- `!` shell mode; `/init` AGENTS.md generation; `/diff` (staged/all/session, cross-surface); `/context` breakdown; `/focus` reduced-output view; Ctrl+S prompt stash; persistent `/goal` indicator; multi-select clarify (checkboxes) across CLI/gateway/TUI ([#​72257](https://github.com/NousResearch/hermes-agent/pull/72257), [#​72178](https://github.com/NousResearch/hermes-agent/pull/72178), [#​72240](https://github.com/NousResearch/hermes-agent/pull/72240), [#​72242](https://github.com/NousResearch/hermes-agent/pull/72242), [#​72302](https://github.com/NousResearch/hermes-agent/pull/72302), [#​72262](https://github.com/NousResearch/hermes-agent/pull/72262), [#​72244](https://github.com/NousResearch/hermes-agent/pull/72244), [#​72188](https://github.com/NousResearch/hermes-agent/pull/72188) — [@​teknium1](https://github.com/teknium1), salvaging [@​SHL0MS](https://github.com/SHL0MS), [@​iRonin](https://github.com/iRonin), [@​gigi206](https://github.com/gigi206) + more)
- `hermes import-agent` — one-command migration from Claude Code / Codex CLI setups ([#​72190](https://github.com/NousResearch/hermes-agent/pull/72190) — [@​teknium1](https://github.com/teknium1))
- Per-turn summary line + live token flow in the spinner; cross-surface theme SDK (one skin themes CLI, TUI, and desktop, live) ([#​72246](https://github.com/NousResearch/hermes-agent/pull/72246), [#​68857](https://github.com/NousResearch/hermes-agent/pull/68857) — [@​teknium1](https://github.com/teknium1), [@​OutThisLife](https://github.com/OutThisLife))
- TUI: reach the model picker without wrecking your draft + mid-turn switching; slash menu leads with your most-used skills; attachments live in the composer; Arabic (ar) locale with RTL across desktop/dashboard/agent ([#​74756](https://github.com/NousResearch/hermes-agent/pull/74756), [#​75931](https://github.com/NousResearch/hermes-agent/pull/75931), [#​75210](https://github.com/NousResearch/hermes-agent/pull/75210), [#​70870](https://github.com/NousResearch/hermes-agent/pull/70870) — [@​OutThisLife](https://github.com/OutThisLife))
- `hermes -w` startup \~14s → \~1.8s; global `--version` fast path; banner update-check 6× faster; dashboard lazy-loads routes + GROUP BY session stats; session filtering tabs (Chats/Automation/All) ([#​71637](https://github.com/NousResearch/hermes-agent/pull/71637), [#​62096](https://github.com/NousResearch/hermes-agent/pull/62096), [#​74188](https://github.com/NousResearch/hermes-agent/pull/74188), [#​72294](https://github.com/NousResearch/hermes-agent/pull/72294), [#​73362](https://github.com/NousResearch/hermes-agent/pull/73362), [#​73865](https://github.com/NousResearch/hermes-agent/pull/73865) — [@​teknium1](https://github.com/teknium1), [@​kshitijk4poor](https://github.com/kshitijk4poor))
- Runtime: Node 26 required across installers/heal/upgrade, managed Node/uv resolve before bare PATH, outdated managed trees heal to target major; brew + pip/PyPI wheel channels retired (shell installer / Docker / Nix are the supported channels) ([#​76459](https://github.com/NousResearch/hermes-agent/pull/76459), [#​68217](https://github.com/NousResearch/hermes-agent/pull/68217) — [@​ethernet8023](https://github.com/ethernet8023))
##### 🧩 Skills, Plugins & MCP
- **A2A v1.0** — Agent-to-Agent protocol plugin (closes [#​514](https://github.com/NousResearch/hermes-agent/issues/514)) ([#​77109](https://github.com/NousResearch/hermes-agent/pull/77109) — [@​teknium1](https://github.com/teknium1))
- Curator: surface unmanaged skills + `curator adopt`; skill-description truncation surfaced to authors; grounded-citations skill (+ fact-checking mode); simplify-code v1.1; tldraw-offline scripting skill ([#​71648](https://github.com/NousResearch/hermes-agent/pull/71648), [#​70519](https://github.com/NousResearch/hermes-agent/pull/70519), [#​71698](https://github.com/NousResearch/hermes-agent/pull/71698), [#​77104](https://github.com/NousResearch/hermes-agent/pull/77104), [#​70440](https://github.com/NousResearch/hermes-agent/pull/70440), [#​66896](https://github.com/NousResearch/hermes-agent/pull/66896) — [@​teknium1](https://github.com/teknium1))
- Office skills bundled: docx, xlsx, pdf + refreshed powerpoint; skills-tree debloat continues (yuanbao, segment-anything, jupyter, heartmula, audiocraft → optional-skills; claude-marketplace source removed; hub restructure absorbing themes/desktop-plugins/tui-widgets) ([#​68595](https://github.com/NousResearch/hermes-agent/pull/68595), [#​70452](https://github.com/NousResearch/hermes-agent/pull/70452)–[#​70456](https://github.com/NousResearch/hermes-agent/pull/70456), [#​73903](https://github.com/NousResearch/hermes-agent/pull/73903) — [@​teknium1](https://github.com/teknium1))
- MCP: Comfy Cloud catalog entry with curated 20-tool default; hidden-whitespace warnings in MCP config; pinecone-research optional skill ([#​66112](https://github.com/NousResearch/hermes-agent/pull/66112), [#​75736](https://github.com/NousResearch/hermes-agent/pull/75736), [#​70512](https://github.com/NousResearch/hermes-agent/pull/70512) — [@​teknium1](https://github.com/teknium1))
- MCP lazy server startup from a fingerprint-keyed on-disk tool-schema cache — configured servers no longer all boot at session start (design from [#​56832](https://github.com/NousResearch/hermes-agent/issues/56832)) ([#​77511](https://github.com/NousResearch/hermes-agent/pull/77511) — [@​kshitijk4poor](https://github.com/kshitijk4poor))
- NeMo Relay observability integration — re-landed after an in-window revert, on stable NeMo Relay 0.6 ([#​67607](https://github.com/NousResearch/hermes-agent/pull/67607) — [@​afourniernv](https://github.com/afourniernv))
- Gateway health & diagnostics OTLP export ([#​64536](https://github.com/NousResearch/hermes-agent/pull/64536) — [@​victor-kyriazakos](https://github.com/victor-kyriazakos))
##### 🔒 Security & Reliability
- Iron-proxy credential-injection egress firewall re-landed ([#​70848](https://github.com/NousResearch/hermes-agent/pull/70848) — [@​teknium1](https://github.com/teknium1))
- DNS-pinned SSRF-safe fetches + Slack CDN allowlist; strict redaction at compaction boundaries; ReDoS eliminated in config-key redaction patterns; prose words embedding a secret keyword no longer masked ([#​70193](https://github.com/NousResearch/hermes-agent/pull/70193), [#​69294](https://github.com/NousResearch/hermes-agent/pull/69294), [#​76083](https://github.com/NousResearch/hermes-agent/pull/76083), [#​67776](https://github.com/NousResearch/hermes-agent/pull/67776) — [@​teknium1](https://github.com/teknium1))
- Tier-3 credential reads scoped (FAL/XAI/VERCEL/DAYTONA/GITHUB presence checks etc.); CVE dependency pins refreshed (cryptography, starlette, python-multipart); hindsight env file 0600; /model moved off the gateway event loop ([#​75888](https://github.com/NousResearch/hermes-agent/pull/75888), [#​72362](https://github.com/NousResearch/hermes-agent/pull/72362) — [@​teknium1](https://github.com/teknium1))
- Windows hardening wave: text-mode subprocess decode bug class closed repo-wide, console flashes hidden across daemons/env probes/LSP/installer paths, residual encoding gaps (MCP stdio, gateway update I/O, STT/TTS, desktop spawn) ([#​70875](https://github.com/NousResearch/hermes-agent/pull/70875), [#​70205](https://github.com/NousResearch/hermes-agent/pull/70205), [#​70264](https://github.com/NousResearch/hermes-agent/pull/70264), [#​71014](https://github.com/NousResearch/hermes-agent/pull/71014) — [@​teknium1](https://github.com/teknium1), salvaging several community PRs)
- State/session integrity: four session-state fixes (safe close tracking, flush-cursor class fix, row-retry, usage-PK healer); compact v23 FTS layout + `hermes sessions optimize` + CJK-bigram FTS; read-path split with per-thread read-only connections ([#​75883](https://github.com/NousResearch/hermes-agent/pull/75883), [#​65798](https://github.com/NousResearch/hermes-agent/pull/65798), [#​69423](https://github.com/NousResearch/hermes-agent/pull/69423), [#​73344](https://github.com/NousResearch/hermes-agent/pull/73344) — [@​teknium1](https://github.com/teknium1), [@​kshitijk4poor](https://github.com/kshitijk4poor))
- OpenViking memory-provider hardening — fail closed on blocked endpoints, server verification before credentials are sent, config.yaml-first settings ([#​77747](https://github.com/NousResearch/hermes-agent/pull/77747) — [@​kshitijk4poor](https://github.com/kshitijk4poor))
- Credential pool: reset-aware primary restore (stay on fallback until the rate-limit window resets) + deferred-refresh locking fixes; FTS UPDATE triggers narrowed with fail-closed CJK migration ([#​77631](https://github.com/NousResearch/hermes-agent/pull/77631), [#​77628](https://github.com/NousResearch/hermes-agent/pull/77628) — [@​kshitijk4poor](https://github.com/kshitijk4poor))
- Config-driven memory allocator trim with telemetry; holographic memory vectors stored float32; loop-invariant HRR encodes hoisted ([#​76905](https://github.com/NousResearch/hermes-agent/pull/76905), [#​76917](https://github.com/NousResearch/hermes-agent/pull/76917), [#​76881](https://github.com/NousResearch/hermes-agent/pull/76881) — [@​kshitijk4poor](https://github.com/kshitijk4poor))
##### 🐛 Notable Bug Fixes
- Voice: full-duplex interruption during generation AND playback; whole-turn desktop speech; auto-TTS delivery gaps ([#​74223](https://github.com/NousResearch/hermes-agent/pull/74223), [#​69936](https://github.com/NousResearch/hermes-agent/pull/69936), [#​73508](https://github.com/NousResearch/hermes-agent/pull/73508) — [@​teknium1](https://github.com/teknium1))
- Desktop: Stop parks the queue instead of firing the next queued prompt; branch-in-new-chat restart loss; false remote-gateway reauthentication; cross-session composer leaks ([#​68725](https://github.com/NousResearch/hermes-agent/pull/68725), [#​71960](https://github.com/NousResearch/hermes-agent/pull/71960), [#​68250](https://github.com/NousResearch/hermes-agent/pull/68250), [#​70986](https://github.com/NousResearch/hermes-agent/pull/70986) — [@​SHL0MS](https://github.com/SHL0MS), [@​alelpoan](https://github.com/alelpoan), [@​helix4u](https://github.com/helix4u), [@​OutThisLife](https://github.com/OutThisLife))
- Gateway: session lists scoped before limiting; relay-backed home delivery after restart; timeline display events persisted ([#​65509](https://github.com/NousResearch/hermes-agent/pull/65509), [#​70102](https://github.com/NousResearch/hermes-agent/pull/70102), [#​69771](https://github.com/NousResearch/hermes-agent/pull/69771) — [@​GodsBoy](https://github.com/GodsBoy), [@​victor-kyriazakos](https://github.com/victor-kyriazakos), [@​ethernet8023](https://github.com/ethernet8023))
- Agent: context-length fallback logging + batch trajectory durability; Codex OAuth context windows revalidated against the live catalog ([#​76027](https://github.com/NousResearch/hermes-agent/pull/76027), [#​68554](https://github.com/NousResearch/hermes-agent/pull/68554) — [@​kshitijk4poor](https://github.com/kshitijk4poor), [@​teknium1](https://github.com/teknium1))
- ...plus roughly 770 more `fix:` PRs across every subsystem this window.
##### 👥 Contributors
**647 contributors** shipped this release (commit authors, co-authors, and salvaged-PR credits).
##### Core
[@​teknium1](https://github.com/teknium1), [@​OutThisLife](https://github.com/OutThisLife) (desktop, voice, perf), [@​kshitijk4poor](https://github.com/kshitijk4poor) (perf, caching, salvage), [@​ethernet8023](https://github.com/ethernet8023) (runtime, E2E, desktop), [@​benbarclay](https://github.com/benbarclay) (relay, HSP, auth)
##### All Contributors (alphabetical)
[@​02356abc](https://github.com/02356abc), [@​0301chris](https://github.com/0301chris), [@​0xAlcibiades](https://github.com/0xAlcibiades), [@​0xDevNinja](https://github.com/0xDevNinja), [@​0xLeathery](https://github.com/0xLeathery), [@​0xprincess](https://github.com/0xprincess), [@​0xr00tf3rr3t](https://github.com/0xr00tf3rr3t), [@​100yenadmin](https://github.com/100yenadmin),
[@​2001Y](https://github.com/2001Y), [@​3ssiri](https://github.com/3ssiri), [@​55nx954gn6-debug](https://github.com/55nx954gn6-debug), [@​686f6c61](https://github.com/686f6c61), [@​87degrees](https://github.com/87degrees), [@​aaronlab](https://github.com/aaronlab), [@​abundantbeing](https://github.com/abundantbeing), [@​Adolanium](https://github.com/Adolanium),
[@​adriansotomora](https://github.com/adriansotomora), [@​adurham](https://github.com/adurham), [@​afourniernv](https://github.com/afourniernv), [@​afurm](https://github.com/afurm), [@​AgenticSpark](https://github.com/AgenticSpark), [@​ahmadashfq](https://github.com/ahmadashfq), [@​AhmetArif0](https://github.com/AhmetArif0), [@​ai-ag2026](https://github.com/ai-ag2026),
[@​AIalliAI](https://github.com/AIalliAI), [@​aider4ryder](https://github.com/aider4ryder), [@​airclear](https://github.com/airclear), [@​ajzrva-sys](https://github.com/ajzrva-sys), [@​akattelu](https://github.com/akattelu), [@​AKAZIK-py](https://github.com/AKAZIK-py), [@​akb4q](https://github.com/akb4q), [@​akshan-main](https://github.com/akshan-main), [@​AlanBurningsuit](https://github.com/AlanBurningsuit),
[@​alelpoan](https://github.com/alelpoan), [@​AlexFucuson9](https://github.com/AlexFucuson9), [@​AlexxRussell](https://github.com/AlexxRussell), [@​AllardQuek](https://github.com/AllardQuek), [@​alt-glitch](https://github.com/alt-glitch), [@​aman-merchant](https://github.com/aman-merchant), [@​amanning3390](https://github.com/amanning3390), [@​amathxbt](https://github.com/amathxbt),
[@​aml1973](https://github.com/aml1973), [@​amoreno16003](https://github.com/amoreno16003), [@​AndrewMoryakov](https://github.com/AndrewMoryakov), [@​andrexibiza](https://github.com/andrexibiza), [@​andynguyendk](https://github.com/andynguyendk), [@​andyylin](https://github.com/andyylin), [@​aneym](https://github.com/aneym), [@​angelos](https://github.com/angelos),
[@​aniruddhaadak80](https://github.com/aniruddhaadak80), [@​AnnasMazhar](https://github.com/AnnasMazhar), [@​annguyenNous](https://github.com/annguyenNous), [@​anoopmehendale-cue](https://github.com/anoopmehendale-cue), [@​AnthonyFrancis](https://github.com/AnthonyFrancis), [@​arcabotai](https://github.com/arcabotai), [@​ArcherQAQ](https://github.com/ArcherQAQ),
[@​Ares4Tech](https://github.com/Ares4Tech), [@​arimu1](https://github.com/arimu1), [@​arnoldfrancisca](https://github.com/arnoldfrancisca), [@​asimons81](https://github.com/asimons81), [@​asorry75](https://github.com/asorry75), [@​AtakanGs](https://github.com/AtakanGs), [@​ATran28](https://github.com/ATran28), [@​austinpickett](https://github.com/austinpickett),
[@​Automata-intelligentsia](https://github.com/Automata-intelligentsia), [@​awain7](https://github.com/awain7), [@​aweiker](https://github.com/aweiker), [@​aydnOktay](https://github.com/aydnOktay), [@​ayushere](https://github.com/ayushere), [@​b](https://github.com/b), [@​baau](https://github.com/baau), [@​baauzi](https://github.com/baauzi), [@​baenregod](https://github.com/baenregod),
[@​bakhtiersizhaev](https://github.com/bakhtiersizhaev), [@​Baophan00](https://github.com/Baophan00), [@​baoyu0](https://github.com/baoyu0), [@​Bartok9](https://github.com/Bartok9), [@​basilalshukaili](https://github.com/basilalshukaili), [@​BB-light](https://github.com/BB-light), [@​bbopen](https://github.com/bbopen), [@​Beandon13](https://github.com/Beandon13),
[@​beardedeagle](https://github.com/beardedeagle), [@​bedirhancode](https://github.com/bedirhancode), [@​benbarclay](https://github.com/benbarclay), [@​benegessarit](https://github.com/benegessarit), [@​benjamin2026-dot](https://github.com/benjamin2026-dot), [@​bennybuoy](https://github.com/bennybuoy), [@​BenSheridanEdwards](https://github.com/BenSheridanEdwards),
[@​BKStock](https://github.com/BKStock), [@​BlackishGreen33](https://github.com/BlackishGreen33), [@​bnikanjam](https://github.com/bnikanjam), [@​bounce12340](https://github.com/bounce12340), [@​Bounty13](https://github.com/Bounty13), [@​bpross](https://github.com/bpross), [@​briandevans](https://github.com/briandevans), [@​bricelb](https://github.com/bricelb), [@​brunopirz](https://github.com/brunopirz),
[@​bryanneva](https://github.com/bryanneva), [@​byshubham](https://github.com/byshubham), [@​camaleonidas](https://github.com/camaleonidas), [@​canorionen](https://github.com/canorionen), [@​carbongotfound](https://github.com/carbongotfound), [@​carljborg](https://github.com/carljborg), [@​carlotestor](https://github.com/carlotestor), [@​carrion256](https://github.com/carrion256),
[@​caseyanthony](https://github.com/caseyanthony), [@​cat-thats-fat](https://github.com/cat-thats-fat), [@​Cdddo](https://github.com/Cdddo), [@​ceverson70](https://github.com/ceverson70), [@​chancelu](https://github.com/chancelu), [@​chaos-xxl](https://github.com/chaos-xxl), [@​CharlesMcquade](https://github.com/CharlesMcquade), [@​chazmaniandinkle](https://github.com/chazmaniandinkle),
[@​chefboyrdave21](https://github.com/chefboyrdave21), [@​chelsealong](https://github.com/chelsealong), [@​Christopher-Schulze](https://github.com/Chr…
randlee
pushed a commit
to randlee/hermes-agent
that referenced
this pull request
Aug 11, 2026
…usResearch#69253) * feat(nous): send top-level session_id for provider sticky routing The Nous Portal profile only embedded the session id inside portal tags, so Claude traffic through the portal had no sticky-routing key. Multi-turn sessions could reroute between upstream endpoints (Anthropic/Vertex/ Bedrock), cold-writing a fresh prompt cache on every reroute since each provider's cache is instance-local. Mirror the OpenRouter profile: emit extra_body.session_id whenever the agent has one, pinning every turn of a session to the same endpoint so explicit cache_control breakpoints stay warm. * test: expect top-level session_id in Nous max-iterations summary body Sibling site of the profile change — the max-iterations summary path builds its request through the same NousProfile.build_extra_body(), so its exact-shape assertion now includes the sticky-routing session_id when the agent has a session.
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.
Summary
Nous Portal Claude traffic now carries a top-level
session_id, giving the portal a sticky-routing key so multi-turn sessions pin to one upstream endpoint and Anthropiccache_controlbreakpoints stay warm.Root cause: the Nous profile only embedded the session id inside portal
tags— with no top-level routing key, sessions fall back to opening-message hashing, which breaks pinning whenever the opening messages shift. Each reroute between upstream endpoints (provider caches are instance-local) cold-writes a fresh prompt cache, dragging the effective cache-hit rate down.Changes
plugins/model-providers/nous/__init__.py:build_extra_body()emitssession_idwhen the agent has one (mirrors the OpenRouter profile).tests/providers/test_provider_profiles.py: session_id present when set / absent when unset.Validation
scripts/run_tests.sh tests/providers/(profiles + transport parity)ChatCompletionsTransport.build_kwargs()extra_body.session_idon wire when set; absent when unsetInfographic