Commit Graph

21384 Commits

Author SHA1 Message Date
Teknium b952a6cd72
fix(middleware): preserve pre-Relay tool request snapshot 2026-08-08 16:17:04 -07:00
cresslank 1ce6d95f20
fix(middleware): chain request rewrites sequentially
Focused salvage of request-middleware composition from PR #73656.

(cherry picked from commit 089f76e821)
2026-08-08 16:12:15 -07:00
hermes-seaeye[bot] 3e6a081d60
fmt(js): `npm run fix` on merge (#82055)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-08 22:51:53 +00:00
teknium1 55982159dd feat(tests): CI-enforce skill authoring standards; clear all remaining debt
New tests/skills/test_authoring_standards.py parametrizes every bundled +
optional SKILL.md (1148 checks) against the mechanically-verifiable subset
of the hardline standards:
- required frontmatter fields (name/description/version/author/license/
  platforms) + tags
- frontmatter name == directory name
- description <= 60 chars, ends with period, no marketing words
- related_skills resolve in-repo
- no machine-local paths
- <= 100k chars
Grandfather dict for legacy debt ships EMPTY — all pre-existing violations
fixed in this PR:

- 13 frontmatter names canonicalized to their directory names (the install
  identifier); all related_skills references updated (comfyui -> stable-
  diffusion). Fixes the class behind PR #42788's report; also fixes
  here.now's invalid dot-name.
- optional-skills/devops/cli -> inference-sh-cli (dir was the generic
  'cli'; fm name was right) incl. docs pages (en + zh-Hans), catalog row,
  sidebar entry.
- pytorch-fsdp: 157k generated 'Quick Reference' dump moved to
  references/common-patterns.md; SKILL.md 159k -> 2.5k with a pointer.
- research-paper-writing: 31.7k Phase 5 drafting section moved to
  references/phase5-paper-drafting.md; SKILL.md 103k -> 71k.

Docs regenerated with scope discipline.
2026-08-08 15:43:00 -07:00
brooklyn! 3898e646e5
Merge pull request #82044 from NousResearch/bb/agent-plugins
Surface agent plugins in the desktop app's Settings → Plugins
2026-08-08 17:41:38 -05:00
Brooklyn Nicholson 44790bc9c8 feat(desktop): plugin descriptions + open the agent plugins folder
HermesPlugin/PluginRecord gain a description one-liner (kanban gets
one) shown in the inventory instead of the raw file path, and the
agent plugins section can open the backend's plugins dir — path from
config.get profile so it's profile-aware, local backends only since
openDir mkdir-creates.
2026-08-08 17:34:20 -05:00
Brooklyn Nicholson ed9eee5dc9 fix(gateway): report bundled auto-loading plugins as enabled
Bundled backends/platforms/providers load without a plugins.enabled
entry ('must just work'), but plugins.manage reported them 'not
enabled' — clients rendered running plugins with an OFF switch.
Surface the truthful default; explicit disable still wins.
2026-08-08 17:34:20 -05:00
teknium1 1c9433897c chore(skills): standards sweep — bring 42 bundled/optional skills up to hardline
Audited all 191 in-repo skills (77 bundled, 114 optional) against the
authoring standards (AGENTS.md hardline + PR #80800). Fixed 42:

- 8 overlong descriptions rewritten to <= 60 chars, one sentence, period
  (one-three-one-rule 525ch, drug-discovery 405ch, web-pentest 358ch,
  fitness-nutrition 352ch, neuroskill-bci 332ch, oss-forensics 320ch,
  computer-use 307ch, memento-flashcards 252ch)
- 24 skills with missing frontmatter fields: author (credited from git
  history: f-trycua, SHL0MS, teyrebaz33, haileymarshall, FurkanL0,
  teknium1), license, version, platforms, tags
- 7 machine-local paths scrubbed (/home/bb, /home/user, /home/ubuntu ->
  portable placeholders)
- 11 marketing-word intros reworded (Comprehensive/state-of-the-art)
- docs catalogs + per-skill pages regenerated, scope-disciplined

Deferred (not in this PR):
- 14 frontmatter/dir name mismatches — open PR #42788 already proposes
  the dir-rename approach for 4 of them; resolve there as one class
- 2 skills over 100k chars (pytorch-fsdp 159k, research-paper-writing
  103k) — need content splits into references/, separate PRs
- comfyui dangling related_skill resolves after the name-mismatch class
2026-08-08 15:27:29 -07:00
Brooklyn Nicholson c86da8397b feat(desktop): agent plugins in Settings → Plugins
Backend plugins — native Hermes plugins and portable Agent Plugins v1
packages — were invisible in the desktop app. Settings → Plugins now
lists them under the desktop (renderer) plugins with source/portable
pills, enable/disable switches keyed by canonical registry key, and a
live-filter search box, backed by a nanostore over plugins.manage.
Categories other surfaces own (dashboard_auth/*, model-providers/*,
platforms/*) are curated out renderer-side.
2026-08-08 17:24:59 -05:00
Brooklyn Nicholson a60b492e07 feat(gateway): key-addressed plugins.manage rows + portable MCP toolset fold-in
plugins.manage list rows now carry the canonical registry key and a
portable flag (Agent Plugins v1 plugin.json packages), and toggles
address the key — bare names collide across category dirs
(image_gen/fal vs video_gen/fal), so name-addressed toggles flipped
both. Portable packages' in-memory MCP servers also fold into
enabled_mcp_server_names(); without that their tools registered with
the MCP runtime but never reached the model's schema.
2026-08-08 17:24:59 -05:00
brooklyn! fe42097865
Merge pull request #81985 from NousResearch/bb/session-titles
Name sessions instantly from the opening message, and make the name stick
2026-08-08 17:17:09 -05:00
Brooklyn Nicholson f726090d48 feat(sessions): name a session the moment it starts
Titling fired on the first response, so a session sat unnamed for the whole
opening turn - p50 151s, p90 1212s across real sessions, because a turn is
tool calls, not one round-trip. A turn that failed or was interrupted never
got a title at all. Four surfaces each carried their own copy of the call.

Move it into the shared turn prologue and split it in two: a deterministic
title derived from the user's opening message, written inline before the
model runs, then one small-model call that upgrades it. The response is
constrained to a JSON object so there is no preamble to strip, and control
wrappers are stripped rather than refused, so a slash command titles as
what the user asked for instead of the command itself.
2026-08-08 17:07:21 -05:00
Brooklyn Nicholson e358eaf44a perf(sessions): resolve the titling model from the provider's live catalog
Titling ran on the user's main chat model, so a five-word title was billed
to a frontier reasoning model and inherited its latency. Pinning a cheap
model id instead just moves the problem: the hardcoded default was already
dead upstream and every call paid a 404 before the retry net caught it.

Match model FAMILIES against the provider's live /v1/models catalog,
preferring rolling '-latest' aliases where a provider publishes them, and
order the families by measured latency. Nothing to bump when a provider
ships a new mini/flash/haiku. Opt-in per task, so compression, vision, and
search keep 'auto means my chat model'.
2026-08-08 17:07:21 -05:00
Brooklyn Nicholson 5566379f57 fix(sessions): give titles provenance so they stop overwriting themselves
A session title had no notion of who set it, so two bugs followed. An
auto-generated title could clobber a name the user typed, and every
compression rotation renumbered the conversation it forked - one piece of
work reaching 'Smallville Map Architecture Plan #10' in the sidebar.

Titles now carry a source (derived < llm < user) enforced by one
compare-and-swap, so an automatic write can only ever replace a title of
strictly lower authority. Compression carries the name across unchanged.
Legacy NULL rows rank as user, so auto-titling only fills genuinely
empty titles on existing data.
2026-08-08 17:07:21 -05:00
brooklyn! 3391836888
Merge pull request #82036 from NousResearch/bb/session-arc-source
perf(desktop): keep session status inside the row's own fiber
2026-08-08 16:58:34 -05:00
brooklyn! 4907e3d900
Merge pull request #81977 from helix4u/fix/desktop-main-window-ready-fallback
fix(desktop): reveal windows after a missed ready-to-show event
2026-08-08 16:58:00 -05:00
Brooklyn Nicholson 41ae3db426 fix(desktop): reveal every window after a missed ready event, not just the main one
Session, instance, HUD, quick-entry and pet-overlay windows all open with
show: false and are revealed only by ready-to-show, so the Electron 40 bug
strands them exactly the way it stranded the primary window — and none of
them have the second-launch workaround that made the main-window case
recoverable.

Generalize the controller to any window and wire all six through one
wireWindowReveal helper. Callers pass their own reveal action (showInactive
for the pet overlay, show + focus for the HUD and quick entry) and their own
post-visible work, so whichever path wins runs them exactly once.

Quick entry now reveals the window the call created rather than whatever
`quickEntryWindow` points at when the event lands.
2026-08-08 16:51:44 -05:00
Brooklyn Nicholson 041cbff5e3 fix(desktop): keep the Playwright reveal path off the production fallback
Desktop E2E is hard-disabled in ci.yml (#76627) because the mock-backend
window never reaches a usable state, so nothing can validate dropping the
TEST_WORKER_INDEX force-show right now — and the suite's lead symptom is
already a window-readiness failure. Restore it, routed through the reveal
controller so the bookkeeping in onRevealed still runs exactly once, and
leave the removal to whoever re-enables the suite.
2026-08-08 16:51:44 -05:00
Brooklyn Nicholson 615a435b7a perf(desktop): stop a settling turn from repainting the whole sidebar
ChatSidebar read $workingSessionIds with useStore purely to notice that a turn
had finished and re-probe worktree lanes. Nothing in its markup used the value,
so every status edge re-rendered the entire sidebar — each section, each row —
to run an effect that touches no DOM.

Listen to the store instead. The rows own their status subscription, so a
session changing color repaints that row's fiber and nothing above it, which a
test now holds in place by counting row renders.
2026-08-08 16:51:25 -05:00
Teknium 66ea4e686d feat(media): default-on upscaling for sub-2MP image models (FAL + Krea)
Per review: upscaling should be the default behavior (like the original
flux-2-pro chain), not agent opt-in. Policy: every image model whose
native output is below ~2MP now sets upscale=True in its catalog —
users never silently get low-res images. Native hi-res models
(Seedream 5 Pro/Lite, Krea 2 Large) stay off to avoid paying to
upscale already-large output.

- FAL catalog: 16 models flipped to upscale=True (klein, z-image,
  nano-banana pro/2/2-lite, gpt-image 1.5/2, ideogram v3/v4, recraft
  v4/v4.1, qwen image/3, krea-2 medium on FAL, MAI 2.5 pro).
- Krea plugin: per-model upscale defaults (medium + medium-turbo ON at
  1.5K native; large OFF at 2K native), precedence explicit kwarg >
  image_gen.krea.upscale config > catalog default.
- The 'upscale' tool param remains as a per-call override in both
  directions (false = fast draft, true = force on hi-res/edits).
- Video unchanged: opt-in only (default-on would double every video's
  cost and latency).
- Sibling tests updated: routing/payload tests pass upscale=False where
  the assertion targets the generation submit; catalog test now pins
  the native-resolution policy instead of the flux-2-pro snapshot.
2026-08-08 14:49:28 -07:00
Teknium 137960c9aa feat(media): opt-in upscale pass for image_generate and video_generate across FAL and Krea
The generated-media surface previously had almost no upscaler coverage:
only fal-ai/flux-2-pro chained Clarity Upscaler (hardcoded catalog
default), every other image model returned ~1MP output with no high-res
path, and video had no upscaler at all. Krea's API treats the enhancer
as a standard second pass; this brings the same shape to Hermes.

- image_generate: new optional 'upscale' boolean in the tool schema.
  Explicit true chains the backend upscaler on ANY model (including
  edits); explicit false disables flux-2-pro's automatic default;
  omitted keeps per-model catalog behavior. Response now reports
  'upscaled' so the agent knows which resolution it got.
- FAL image path: explicit flag overrides the catalog 'upscale' default
  (Clarity Upscaler, 2x). Failure falls back to the native image.
- Krea plugin: upscale=true chains Krea Enhance
  (/generate/enhance/krea/enhance, 2x, prompt-guided) through the same
  BYO/managed base URL + auth as generation, with a best-effort poll
  loop that never fails a successful generation.
- video_generate: new optional 'upscale' boolean; FAL video plugin
  chains ByteDance SeedVR2 (fal-ai/seedvr/upscale/video, 2x factor
  mode). Providers without upscalers ignore the kwarg per the ABC
  contract (documented in both ABCs).

Validation: targeted suites green (123 tests across 6 files, including
new coverage for override-wins/default-kept/failure-fallback on all
three paths); live E2E on direct FAL verified both chains end-to-end
(klein 9b + Clarity upscaled image; pixverse-v6 1s 360p + SeedVR2
upscaled video).
2026-08-08 14:49:28 -07:00
Brooklyn Nicholson b07ee44f1b refactor(desktop): let the sidebar row read its own status
The dot resolved its state through $sessionDotStateById while the arc on the
same row was decided from an isWorking prop, drilled from the sidebar through
two list components and asserted in five test setups. Two paths to the same
question is how the row's arc and its dot end up disagreeing, and it is why the
arc has broken independently of the dot before.

The row now reads the resolved state directly, and the arc rule moves next to
the states it talks about as `showsRunningArc`. `hasLiveTurn` keeps the row's
other treatment — brighter title, age yielding to the actions menu — on the
wider meaning it always had, where a turn waiting on an answer still counts as
this session's turn.

The list chain drops the prop, its types and the id set built to feed it.
`$workingSessionIds` stays where the sidebar genuinely needs it, for noticing
that a turn settled.
2026-08-08 16:42:00 -05:00
brooklyn! 71326399d3
Merge pull request #81991 from NousResearch/bb/session-status-dot
fix(desktop): make the session status dot mean one thing
2026-08-08 16:33:39 -05:00
brooklyn! 51597c5e07
Merge pull request #82007 from NousResearch/bb/hud-surface-note
The agent knows when it's floating in HUD mode, and looks at the app underneath
2026-08-08 16:28:35 -05:00
Brooklyn Nicholson f04ad5a829 style(desktop): drop the pulsing glow behind the status dot
Nothing renders it now that the dot holds still, and the row markup no longer
has to opt out of overflow clipping to leave room for the halo.
2026-08-08 16:26:48 -05:00
Brooklyn Nicholson 302ee80b6f refactor(desktop): give the status dot one source of truth and a quieter look
Priority between the overlapping signals — a session can be working and unread
and running a background job at once — was resolved at the call site from five
separate membership lookups, which is how surfaces drift apart. `$sessionDotStateById`
does it once and hands each surface a single answer.

The dot's visual language collapses to three colors on one fill/hollow axis
with nothing moving. Motion on a six-pixel circle can only say "something is
happening", which the row's arc already says better, and it cost a repaint per
frame on every row at once; filled now means producing and hollow means open
but quiet. Working and stalled had differed by 30% opacity and were in practice
the same dot. A settled session paints its project color or nothing, rather
than a grey mark of the same weight as a real status next to every resting row.

The switcher had grown its own dot with its own three states, so it disagreed
with the sidebar on the same session. It renders the shared one now.
2026-08-08 16:26:48 -05:00
Brooklyn Nicholson b70c5cadb3 fix(desktop): stop the session status going idle while the turn is running
The status sets are published under a session's current stored id, but the
sidebar row, a persisted tile and the route can each be holding a different
tip of the same lineage after a compression, and every consumer tested
membership with a plain equality check. When the tips disagreed the session
fell out of the working set mid-turn and the dot dropped to idle with the
model still going. Publish each state under every id the conversation answers
to instead, via a shared `lineageAliases` helper.

A conversation that has not been persisted yet has no stored id at all, and
the projection dropped those rows outright, so the first turn of a new chat
showed no dot and no row arc until the backend handed an id back. Fall back to
the runtime id, which until persistence is the same value the surfaces key on.

Background polls could also clear a live busy state before the backend had
caught up with a just-submitted turn, flicking the dot idle for a beat; the
stream path already guards against that, so the poll path now does too.

The stalled watchdog fired at eight minutes, well past the point of being
useful as a hint. Five is past the app's own long-but-healthy silences, like
a typecheck or a full test run, without outlasting the user's patience.
2026-08-08 16:26:48 -05:00
Brooklyn Nicholson 0665cd4b5b style(hud): tighten the surface-note comments and test helper
Comment wording only, plus the desktop test's boolean parameter becomes
an 'app' | 'hud' union so the call site says which window it means.
2026-08-08 16:21:15 -05:00
brooklyn! 8e9ecc1f3e
Merge pull request #82014 from NousResearch/bb/hud-band-hit-area
HUD: only take the mouse where the HUD actually is
2026-08-08 16:20:12 -05:00
Teknium e00965a7e8 fix(compression): correct prune boundary + exempt native compaction checkpoints
Two corrections on top of the #71077 base (the whole bug class):

1. Turn boundary = last USER message, not last assistant message. A Codex
   turn spans several assistant messages (assistant+tool_calls -> tool ->
   ... -> final assistant) whose reasoning items must replay together; the
   last-assistant boundary would strip reasoning mid-chain from the active
   turn (the gap flagged in PR #71077 review).

2. type="compaction" checkpoints (native server-side compaction, PR #81747)
   are exempt: they carry already-pruned history, not per-turn reasoning.
   Pruning filters items instead of popping the sidecar key.

Sibling site fixed in the same class: the Codex incomplete-continuation
dedup path blind-overwrote codex_reasoning_items on visually-duplicate
interim messages, which would drop the only copy of a checkpoint captured
on the earlier response. Extracted merge_interim_reasoning_items() into
agent/native_compaction.py; newer reasoning wins, prior checkpoints are
preserved unless the newer payload carries its own.
2026-08-08 14:09:41 -07:00
webtecnica adf9549cdd fix(compression): prune stale codex_reasoning_items during compaction (#71058) 2026-08-08 14:09:41 -07:00
teknium1 65710ca186 chore(skills/competitor-news-monitor): cron-recipe shape + competitor-watch blueprint
Skill polish (hardline standards):
- description 247 -> 55 chars; author credits Ben Barclay (benbarclay) first
- restructured into Setup (foreground, once) / Tick (each scheduled run)
  phases with explicit cronjob(action='create') wiring and a state file
  at ~/.hermes/competitor-watches/
- dropped dangling 'change-monitor-and-notify' related_skills entry
- Hermes-tool framing (web_search, web_extract, blogwatcher for feeds)
- coverage honesty: source failure = unknown coverage, cutoff advances
  only on success

Blueprint half:
- new 'competitor-watch' Automation Blueprint (companies/categories/time/
  recurrence/deliver slots) loading the skill, [SILENT] no-news path,
  catalog now 16 blueprints; blueprints index regenerated

Tests: 12 skill tests incl. setup/tick split, coverage-honesty guards,
blueprint registration, and the catalog-wide skills-resolve invariant.
2026-08-08 14:09:22 -07:00
Ben Barclay 309c9bbbe9 feat(skills): add competitor-news-monitor 2026-08-08 14:09:22 -07:00
Teknium 5f4a7e99f0 fix: explain provider DNS failures as possible offline state 2026-08-08 14:08:44 -07:00
Teknium bdfdd2773f chore(deps): reconcile website lock after Algolia search migration rebase
Rebase over 60942fc786 (local lunr search -> Algolia DocSearch) briefly
resurrected the removed @easyops-cn dependency tree from the stale
lock; fresh npm install drops it again. CVE pins unchanged.
2026-08-08 14:06:48 -07:00
Teknium a1e4fee33d fix(deps): enforce 14-day aging + exact pins on all bumped versions
Audit of every changed version across all 4 npm locks + uv.lock
against the >=2-week supply-chain aging rule:

- ip-address: 10.4.0 (8d, and a minor feature release) -> 10.3.1
  (exactly 14d, the CVE-fix patch for GHSA-mwp4-54f8-5fhr)
- nanoid: floating resolutions pulled 3.3.18/6.0.1 (1d/5d, post-fix
  releases) -> scoped overrides nanoid@^3=3.3.17 (the CVE-fix
  version) and nanoid@^6=6.0.0 (27d)
- js-yaml/undici: scoped overrides pin the exact CVE-fix versions
  so transitive copies can't float to newer releases
- postcss: 8.5.25/8.5.26 (2d, npm update drift) -> pinned 8.5.23 (15d)
- @electron/get 5.1.0 (12d) -> 5.0.0 (108d),
  @electron-internal/extract-zip 1.0.5 (10d) -> 1.0.4 (45d) —
  electron 40.10.6's carets resolved to sub-14d releases; both were
  incidental drift, not CVE fixes
- website: nanoid override 3.3.17

Remaining sub-14d versions are exclusively documented CVE-fix
exceptions (.npmrc min-release-age-exclude entries with removal
dates): brace-expansion 5.0.9, dompurify 3.4.13, js-yaml 4.3.1,
mermaid 11.16.1, nanoid 3.3.17, fast-uri 3.1.5, h2 4.4.1 (pyproject
exclude-newer exception).

Rescan: 18 findings (blocked-upstream only). npm run check green.
2026-08-08 14:06:48 -07:00
Teknium 45f31de4e9 fix(deps): mirror aiohttp 3.14.3 pin into lazy_deps feature specs
tests/test_project_metadata.py and test_packaging_metadata.py enforce
that lazy_deps.py exact pins match pyproject extras and uv.lock.
2026-08-08 14:06:48 -07:00
Teknium 7537de9e74 fix(deps): patch 31 known CVEs across Python and npm lockfiles
OSV weekly scan reported 50 known vulnerabilities in pinned deps.
This bumps everything with a released, semver-compatible fix:

Python (uv.lock):
- aiohttp 3.14.1 -> 3.14.3 (GHSA-cq5v-8q36-5273, GHSA-mfx4-hv73-q22v,
  GHSA-mq44-7p77-q5h7)
- h2 4.3.0 -> 4.4.1 (CVE-2026-71554 request smuggling; exclude-newer
  exception documented in pyproject, remove after 2026-08-17)

npm (root workspace):
- brace-expansion 5.0.8 -> 5.0.9, undici 6.27->6.28 / 7.28->7.29,
  js-yaml 4.3.1, nanoid 3.3.17/3.3.18, ip-address 10.4.0,
  mermaid 11.16.1 + dompurify 3.4.13 (root overrides so the
  streamdown transitive copy is pinned too)
- electron 40.10.2 -> 40.10.6 (GHSA-r4w5-6pfg-jxp5; the 41.x major
  for GHSA-9f4c-93c8-jc8g is deferred to its own PR)

npm (website): mermaid, dompurify, js-yaml, nanoid, fast-uri 3.1.5,
postcss 8.5.23, undici 7.29.0
npm (photon sidecar): @opentelemetry/core 2.8.0 via override, undici
npm (whatsapp-bridge): body-parser 1.20.6

min-release-age excludes added to .npmrc/website/.npmrc for the
sub-2wk CVE-fix releases, each with a removal date.

Remaining findings are blocked upstream: cryptography <49 cap
(alibabacloud-tea-openapi), image-size (no fixed release), tar 6.x
transitive majors, electron 41.

Local rescan: 50 -> 19 known vulns, 0 introduced.
2026-08-08 14:06:48 -07:00
Brooklyn Nicholson 717b49c084 fix(desktop): read "is the cursor over the HUD" off the tree, not off a list
The hit test excluded <body> and <html> and missed `#root`, which is
full-window and hit-testable, so every point in the window came back as
something and the window never went mouse-transparent at all. Ask it
structurally instead: anything that CONTAINS the shell is scaffolding around
the HUD rather than part of it, which covers the mount, the body and the
document in one predicate and cannot be out of date again.

Focus gets the same treatment. #81552 pinned the window solid whenever
anything in it held focus, to stop the HUD going click-through under its own
dialogs — but the composer holds focus as the HUD's resting state, so an
engaged HUD claimed its whole rectangle. What that fix needed was focus
BESIDE the shell: a portalled dialog, popover or menu owns the next click,
including the one outside it that dismisses it, and the hit test cannot see
that one coming. Focus inside the shell is the composer, and the hit test
already covers everything the composer can reach.

The decision is a pure function now, so it can be tested against a real DOM
instead of inferred from the effect.
2026-08-08 15:46:40 -05:00
Brooklyn Nicholson 48d672e493 fix(desktop): the HUD only takes the mouse where the HUD actually is
Follow-up to #81920, which bounded the frost to the sheet and left the surface
underneath it unbounded. Nothing paints in the empty space above a short
transcript now, and clicks still die there.

Two reasons, both in this stylesheet. The shell's scaffolding — the shell
itself, the chat surface, the wrapper between them — is full-window,
invisible and hit-testable, so the click-through hit test found something at
every point in the window. And the band's box is the whole window by design
(it is the scroll container), so engaging the HUD turned that entire rectangle
into a click target, which on a fresh thread is a window-sized hole over
whatever you were working in.

So: default the shell to `pointer-events: none` and let surfaces opt in, and
clip the band's box to the sheet, which hit-testing honours. Opting in rather
than listing the scaffolding to exclude, because the scaffolding is not a list
anyone maintains — one more wrapper and the dead rectangle is back, whereas a
control that forgets to opt in is visibly dead.
2026-08-08 15:46:26 -05:00
Brooklyn Nicholson e24bac49fa feat(desktop): tell the agent when it is floating in HUD mode
In HUD mode Hermes is a strip over the app the user is actually working
in, so "what's under you?" or "look up the weather" is almost always
about that app — but the agent had no way to know it was floating, and
answered from its own browser and panes instead.

The desktop tags a HUD submit with `surface: 'hud'` and the gateway turns
that into a per-turn note pointing at read_window_below, and at carrying
the work out in the app underneath. It rides the model-bound message
beside the reaction and speech-interrupted notes rather than the system
prompt: one session can be driven from the app window on one turn and the
HUD on the next, and the system prompt has to stay byte-stable.

Every tool the note names is checked against the agent's own schema
first, so a session without computer_use or read_window_below is never
pointed at a tool it cannot call.
2026-08-08 15:37:41 -05:00
Brooklyn Nicholson 2c94e3fb63 refactor(gateway): one helper for prefixing per-turn notes onto model input
The speech-interrupted and reaction notes each hand-rolled the same
string / multimodal-list prepend. Collapse both onto _prepend_note, which
also gives the "model input only, never persisted, cache-safe" contract a
single place to be written down.
2026-08-08 15:37:34 -05:00
hermes-seaeye[bot] 3da72f1fd1
fmt(js): `npm run fix` on merge (#82000)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-08 20:29:52 +00:00
hermes-seaeye[bot] a726a4aee6
fmt(js): `npm run fix` on merge (#81997)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-08 20:20:18 +00:00
brooklyn! 6f1870cd11
Merge pull request #81956 from NousResearch/bb/new-session-workspace
fix(desktop): a new chat stops landing in the project you were just in
2026-08-08 15:11:06 -05:00
Austin Pickett e0c3caf3b8
fix(model-picker): serve cached custom-provider catalog on no-probe opens (supersedes #81665, #81556) (#81973)
* fix(model-picker): serve cached custom-provider catalog on no-probe opens

#58183 stopped GUI picker opens from live-probing saved custom
OpenAI-compatible endpoints so a stopped local server could not stall the
picker. It gated the whole discovery block, not just the network call, so
`cached_fetch_api_models()` was skipped too — and with it the catalog an
earlier probe had already written to `provider_models_cache.json`.

A custom endpoint that is not the current provider therefore renders only
the models named in its config entry. A local server with 8 models loaded
shows the 1 model that was saved when the provider was first added, on
every picker open, while an explicit Refresh shows all 8.

Add `cache_only` to `cached_fetch_api_models()`: answer from disk within
the existing stale-serve window, never fetch, never revalidate off-thread,
return None on a miss. Split the three call sites in
`list_authenticated_providers()` into what the user's config permits
(`discover_models`, an explicit `models:` allowlist) and how we may obtain
it, so suppressing the probe now downgrades to a cached read instead of
skipping discovery outright. `discover_models: false` still pins, and a
cache hit no longer writes back to config since the probe that populated
it already did.

The latency win stands: a cold cache is a miss, so picker opens against
offline endpoints still make zero network calls.

* test(model-picker): pin the cached-catalog contract for no-probe opens

Cover both halves of the invariant, since fixing either one alone
reintroduces a bug the other guards against.

`cache_only` on `cached_fetch_api_models()`: a fresh entry and an entry
past its TTL but inside the stale-serve window both serve; an entry beyond
that window, an empty cache, rotated credentials, `force_refresh`, and a
missing base_url are all misses — and none of them fetch or spawn a
background revalidation.

`list_authenticated_providers()` on the GUI path: a non-current endpoint
with a warm cache reports its full catalog across all three provider
shapes (`custom_providers`, `providers:`, bare `provider: custom`) with no
live fetch attempted. A cold cache keeps the configured list and still
makes no network call, which is the #58183 guarantee. `discover_models:
false` keeps pinning, and a cache hit does not write back to config.

* fix: persist discovered custom-provider models in the hermes model flow

The `hermes model` named-custom-provider flow (_model_flow_named_custom)
probes the endpoint and shows the full catalog, but never persists it to the
entry's `models:` list. No-probe surfaces (dashboard, desktop, ACP) call
build_models_payload(..., probe_custom_providers=False) and only render the
configured `models:` list, so a provider added via `hermes model` collapses
to the single `model:` default everywhere except the CLI. OpenAI-compatible
providers added via a probing picker already benefit from
_save_discovered_models_to_config; the CLI flow did not.

Persist the live catalog after a successful probe, mirroring the picker path
in model_switch.py. A failed save is non-fatal.

* fix(model-picker): stop an auto-saved catalog pinning a keyless endpoint

The cached-catalog read added for no-probe picker opens still sat behind
the no-key discovery gate, so it never reached the shape that motivated
it: a keyless local model server.

`bool(api_key) or not has_explicit_models` is a network-cost gate. It
exists so Hermes does not probe an endpoint it cannot authenticate to
when that endpoint already declares its catalog (5f00f36ba, 1039e90b5).
Reading a catalog an earlier probe already paid for costs nothing, so
the gate belongs on the probe, not on discovery as a whole.

Left on the discovery side it re-pins the endpoint it was meant to
spare. A successful probe calls `_save_discovered_models_to_config()`,
which writes a plain list into `models:` — exactly the shape
`_models_config_is_allowlist()` reads back as an explicit user
allowlist. A keyless server therefore froze on the catalog of its first
probe and could never widen again, which is the "lineup changes after
config was written" case. f66319097 already carved the dict shape out of
this trap for the same reason; the list shape is the other door into it.

Move the clause to `_probe_live` at both custom-endpoint sites. Probe
suppression is unchanged — verified byte-identical to main across the
keyed/keyless x declared/undeclared matrix — and `discover_models: false`
remains the documented way to pin a catalog.

* test(model-picker): cover the keyless auto-save pinning trap

Three tests around the gate move, each failing on the code before it:

- a keyless endpoint carrying an auto-saved `models:` list still reads
  its full cached catalog
- the same row, cold cache and probing enabled, still makes zero live
  fetches — the network-cost gate the clause exists for
- an end-to-end round trip: persist a probe result via
  `_save_discovered_models_to_config()`, reload it, and assert the shape
  we wrote does not read back as a user pin

The round-trip test guards the whole chain rather than one branch, so a
future change that makes the saved shape look like an intentional
allowlist fails here even if the gate logic is refactored.

* fix(model-picker): key the custom-endpoint model cache by api_mode

`cached_fetch_api_models()` fingerprints entries with `api_mode`, but no
call site in `list_authenticated_providers()` passed it, so every custom
row resolved to the `api_mode=None` fingerprint. Two rows sharing a
base_url and credential but differing by `api_mode` are deliberately
distinct picker rows — it is part of `group_key` at both sites — yet they
collapsed onto one cache entry.

That was latent while probing was the only way to fill a row: a mismatched
entry was overwritten by the row's own live fetch. Serving that entry
without a probe makes it visible, so an `anthropic_messages` row could
render the catalog an OpenAI-mode row cached against the same URL. The
wire protocols differ (`x-api-key` + `anthropic-version` vs
`Authorization: Bearer`), so those catalogs are not interchangeable.

Persist `api_mode` on the group at both grouping sites — it is already
part of `group_key`, so it is constant across the group — and pass it
into the cache read. Section 3b (bare `provider: custom`) has no
`api_mode` in scope and already reads with the empty-credential
fingerprint, so it is unchanged.

Reported by Copilot review on #81973.

---------

Co-authored-by: xxxigm <tuancanhnguyen706@gmail.com>
Co-authored-by: Navlem <114683850+Navlem@users.noreply.github.com>
2026-08-08 16:07:03 -04:00
Austin Pickett 2382f50f53
fix(dashboard): bound WS ticket minting on the events + PTY sockets (supersedes #81931) (#81978)
* fix(dashboard): retry stalled events feed reconnects

* fix(dashboard): bound the PTY ticket request before the socket exists

ChatPage's connect awaits a single-use ticket from `api.buildWsUrl()`
before `new WebSocket()`. That request produces no socket, so a
rejection or a hang emits no `close` event and never arms
PTY_CONNECTING_TIMEOUT_MS (set after the socket is constructed). The
tab stranded on "connecting" with `connectInFlightRef` stuck true,
which also suppresses the page-resume reconnect path.

Give the ticket phase its own deadline and route both failure modes
into the existing backoff. A `ticketSuperseded` flag invalidates a late
ticket result so a timed-out attempt cannot open a socket behind the
replacement it scheduled, and cleanup clears the timer on unmount.

`scheduleReconnect` now takes `number | null` so an attempt that died
before any socket existed omits the "(code N)" banner suffix instead of
inventing one.

Same bug class as the events-feed fix in the preceding commit, on the
main chat surface.

Co-authored-by: Gille <4317663+helix4u@users.noreply.github.com>

* test(dashboard): cover the PTY ticket connect deadline

Mirrors the events-feed cases in ChatSidebar.test.tsx: a rejected ticket
retries, a stalled ticket times out and its late resolution cannot open
a superseded socket, and a settled ticket disarms the deadline so
PTY_CONNECTING_TIMEOUT_MS remains the only guard on a wedged handshake
(NS-591 regression).

Both failure cases fail against ChatPage.tsx without the preceding fix.

Co-authored-by: Gille <4317663+helix4u@users.noreply.github.com>

---------

Co-authored-by: Gille <4317663+helix4u@users.noreply.github.com>
2026-08-08 16:01:02 -04:00
Brooklyn Nicholson c96b978d54 fix(desktop): drop a stale comment describing the removed inheritance step 2026-08-08 15:00:44 -05:00
Brooklyn Nicholson bd39673b8f refactor(desktop): one path-comparison helper instead of two
The Windows-aware path matching added for project ownership was a second
copy of what the file tree's IPC layer already had — same Windows test, same
containment check, one of them carrying a trailing-slash branch its own
normalisation made unreachable. Both now share lib/path-compare.
2026-08-08 14:50:43 -05:00
Teknium 81413f0077 docs: explain model refusal attribution 2026-08-08 12:44:18 -07:00