Commit graph

11223 commits

Author SHA1 Message Date
Teknium
ea7981eba7
fix(dashboard): point webhook-disabled hint at Channels page (#43324)
The webhook 'platform disabled' card told users to enable it 'in your
messaging settings' — no such page exists. The webhook platform is
enabled on the Channels page (nav label), matching how every other
dashboard page refers to it.
2026-06-09 22:41:52 -07:00
kshitij
f1b8519670
Merge pull request #43322 from kshitijk4poor/fix/langfuse-redact-base64-data-uri
fix(langfuse): redact base64 data URIs instead of truncating into invalid base64
2026-06-09 22:41:41 -07:00
mnajafian-nv
f8fd30942c
fix(cli): prevent duplicate one-shot finalize on interrupted cleanup (#43320)
Signed-off-by: mnajafian-nv <mnajafian@nvidia.com>
2026-06-09 22:41:04 -07:00
teknium
1967c590ed
chore: add AUTHOR_MAP entry for xiaoxinova
Maps xiaoxingitee@gmail.com -> xiaoxinova so the contributor-attribution
CI check passes when PR #42342 (MiniMax-M3 1M context fix) is merged.
2026-06-09 22:35:38 -07:00
LeonSGP
702f4df194
Repair cron ownership on container restart (#41976) 2026-06-10 15:32:34 +10:00
kshitij
0092015496
Merge pull request #43323 from kshitijk4poor/fix/skill-view-frontmatter-name-lookup
fix(skills): resolve skill_view by frontmatter name when dir name differs
2026-06-09 22:31:19 -07:00
kshitijk4poor
9caa12f4ec fix(skills): resolve skill_view by frontmatter name when dir name differs
skills_list() surfaces each skill's frontmatter `name:`, but skill_view()
only matched on the on-disk directory name (Strategy 2). When a skill's
directory is a shorter category/alias that differs from its frontmatter
name, skill_view(name) failed to find it. Extend the recursive Strategy-2
walk to also match frontmatter `name:`, guarded by a try/except so an
unreadable/malformed SKILL.md can't break discovery.

Adds a regression test that creates a skill whose directory name differs
from its frontmatter name and asserts skill_view resolves it (fails on
current main, passes with this change).

Salvaged the skill_view fix from #39682 onto current main as a standalone,
single-concern change with the test the original PR lacked.

Co-authored-by: foras910521-lab <foras910521-lab@users.noreply.github.com>
2026-06-10 10:51:45 +05:30
kshitijk4poor
4642762289 fix(langfuse): redact base64 data URIs instead of truncating into invalid base64
The Langfuse SDK treats `data:*;base64,...` strings as media and tries to
decode them. `_truncate_text` was slicing those strings mid-payload, producing
invalid base64 and noisy "Error parsing base64 data URI" logs. Observability
only needs the metadata, not raw image/audio bytes, so redact the whole data
URI (type, media_type, length) before it reaches the SDK.

Salvaged the Langfuse fix from #39682 onto current main as a standalone,
single-concern change (the dashboard `dist/**` and plugin-discovery parts of
that PR already landed separately on main).

Co-authored-by: foras910521-lab <foras910521-lab@users.noreply.github.com>
2026-06-10 10:49:36 +05:30
brooklyn!
bf7abc2f73
Merge pull request #43292 from NousResearch/bb/vscode-marketplace-themes
feat(desktop): install any VS Code theme from the Marketplace
2026-06-09 23:53:59 -05:00
mnajafian-nv
d03cdd63eb
fix(cli): run one-shot query cleanup before lease release (#43036)
* fix(cli): run one-shot query cleanup before lease release

Signed-off-by: mnajafian-nv <mnajafian@nvidia.com>

* test(cli): cover quiet one-shot cleanup finalization

Signed-off-by: mnajafian-nv <mnajafian@nvidia.com>

---------

Signed-off-by: mnajafian-nv <mnajafian@nvidia.com>
2026-06-09 21:52:13 -07:00
Teknium
96af61b6ef
feat(memory,skills): approve/deny gate for memory + skill writes (#38199)
Adds memory.write_mode and skills.write_mode (on|off|approve), applied to
both foreground turns and the background self-improvement review fork — the
source of the unprompted 'wrong assumption' saves users reported.

- on (default): write freely, unchanged behaviour
- off: never write; the tool returns a clean disabled result
- approve: don't commit. Memory foreground writes prompt inline (small,
  reviewable in a chat bubble); background memory writes and ALL skill writes
  stage to a pending store instead (a SKILL.md is too large to review inline,
  and a daemon thread can't block on a prompt)

Review staged writes from CLI or any messaging platform:
  /memory pending|approve|reject|mode
  /skills pending|approve|reject|diff|mode

Skill review respects the size asymmetry: inline you see a one-line gist;
the full unified diff stays out-of-band (/skills diff, dashboard, or the
staged JSON file).

New: tools/write_approval.py (gate + pending store), hermes_cli/
write_approval_commands.py (shared CLI+gateway handlers). Gates wired at the
single entry points memory_tool() and skill_manage(), using the existing
write-origin ContextVar to distinguish foreground from background_review.
2026-06-09 21:51:43 -07:00
Brooklyn Nicholson
7803cbfbb9 style(desktop): use the nous overlay surface (--stroke-nous + --shadow-nous) for the HUDs
Drop the ad-hoc border + shadow-xl for the design-system borderless-overlay
pair already used by the dialog, keybind panel, and notification stack.
2026-06-09 23:49:02 -05:00
Brooklyn Nicholson
45e1689c03 fix(desktop): apply the shared HUD tokens to the marketplace submenu
The 'Install theme…' page is the one palette page rendered as a bespoke
component rather than through the shared CommandItem loop, so it missed the
compact HUD sizing. Route it through HUD_ITEM/HUD_TEXT and top-align the row
icon + status with the title line.
2026-06-09 23:43:29 -05:00
Teknium
fdc90346ea
chore(skills): move red-team skills (godmode, obliteratus) to optional-skills — Anthropic classifier (#43221)
* chore(skills): remove red-team skills (godmode, obliteratus) from bundled catalog

Anthropic's output classifier on claude-fable-5 (and likely other Claude
models served through it) intermittently returns empty content for sessions
whose system prompt advertises these skills. The bundled skills-catalog block
is injected into every session's system prompt, so the descriptions

  - red-teaming/godmode      'Jailbreak LLMs: Parseltongue, GODMODE, ULTRAPLINIAN'
  - mlops/inference/obliteratus 'OBLITERATUS: abliterate LLM refusals (diff-in-means)'

trip the classifier on EVERY session regardless of which skill is actually
loaded, killing unrelated legitimate work (PR review, codebase audits, etc.).

Measured impact (controlled, interleaved A/B, claude-fable-5 via OpenRouter,
prompts differing only by the ~204 chars of these catalog lines, N=20 each):
  catalog lines present -> 19/20 (95%) blocked
  catalog lines absent  -> 5/20  (25%) blocked

Removing them ~quartered the block rate. Rewording the descriptions was not
enough; the skills must leave the bundled catalog.

- Delete skills/red-teaming/godmode and skills/mlops/inference/obliteratus
- Drop their generated doc pages + catalog/sidebar entries (EN + zh-Hans)
- Drop the godmode hand-written-page exception in generate-skill-docs.py

* chore(skills): relocate godmode + obliteratus to optional-skills

Rather than deleting outright, move both into optional-skills/ so they remain
installable via `hermes skills install` while leaving the always-injected
bundled catalog (which is what tripped Anthropic's classifier).

- optional-skills/security/godmode  (was skills/red-teaming/godmode)
- optional-skills/mlops/obliteratus  (was skills/mlops/inference/obliteratus)
- regenerate optional-skills catalog + sidebar entries
2026-06-09 21:41:00 -07:00
Teknium
f082b4ec5c
fix(ci): make parallel runner's exit-4 retry robust for newly-added test files (#42994)
The per-file test runner re-runs a file once when pytest exits 4 ("file or
directory not found") while the file exists on disk — a transient seen on
loaded shared CI runners where the planner collects a file (--collect-only
counts its tests) but the per-file subprocess fails to stat it moments later.

A single immediate retry could land in the same brief high-load window and
fail again, and the retry was gated on one Path.exists() check that can itself
be a flaky stat under that load — so a freshly-added test file that LPT pins to
one shard would deterministically red that shard on every run (no actual test
failure; the file just never executes).

- Extract the subprocess spawn/communicate/process-tree-kill logic into a
  shared _spawn_pytest_once() helper (removes ~90 lines of duplication between
  the primary run and the retry).
- Replace the single-shot retry with a bounded backoff loop
  (_EXIT4_RETRY_ATTEMPTS, escalating sleep) that re-runs while the file is
  present on disk.
- Add _file_present() which re-checks existence across a few spaced stats, so a
  single flaky negative stat doesn't wrongly conclude the file is missing. A
  genuinely-missing file (typo/deleted) still fails fast — exit 4 is not
  swallowed when the file truly does not exist.
- Tests: transient-then-pass recovery, genuinely-missing fails fast with no
  retry, give-up after max attempts, and _file_present transient/missing cases.
2026-06-09 21:39:09 -07:00
Brooklyn Nicholson
833410e02b feat(desktop): theme the terminal ANSI palette + restyle the Cmd-K / Ctrl-Tab HUDs
Imported VS Code themes now carry their integrated-terminal ANSI palette
(`terminal.ansi*`), keyed to the painted variant (terminal / darkTerminal).
The terminal adopts it when the full base-8 set is present and keeps its VS
Code defaults otherwise; withSurface still owns the background, so the pane
stays translucent.

Pull the command palette and session switcher into a shared top-center HUD
(`floating-hud.ts`): no dim/blur backdrop, one compact text + item-padding
size, sidebar-label-style section headers (brand-tinted, uppercase), and the
themed portal scrollbar.
2026-06-09 23:37:50 -05:00
Teknium
6b330522e1
docs(agents): add Design Philosophy + Contribution Rubric to AGENTS.md (#42641)
AGENTS.md was almost entirely how-to/mechanics with the want/don't-want
guidance implicit and scattered. Adds a single authoritative intent layer
near the top, calibrated against what actually merges and what actually
gets rejected.

- 'What Hermes Is': framing + the two properties that drive design
  (prompt-cache integrity, narrow-waist core).
- 'Contribution Rubric': dual-purpose intent doc — (1) for humans/own work:
  what gets merged vs rejected; (2) for the triage sweeper: when a PR is safe
  to close on the three allowed reasons AND when NOT to close one. Taste-based
  'won't implement / out of scope' closes stay human-only by design.
  - 'What we want' calibrated against the last ~55 merges: fix real bugs well,
    expand reach at the edges (platforms/channels/providers/models/desktop —
    large features land routinely), refactor god-files into clean modules,
    keep the CORE narrow. 'Expansive at the edges, conservative at the waist.'
  - 'What we don't want': speculative hooks, .env-for-non-secrets, needless
    core tools, lazy-read escape hatches, feature-destroying fixes, ungated
    telemetry, change-detector tests, core-touching plugins.
  - 'Before you call it a bug — verify the premise (and when NOT to close)':
    distilled from real closes (#41741 intentional-design-not-a-gap, #41610
    wrong-premise, #42327 fix-never-executes, #42393 deliberate-omission,
    #41999 overreach). Doubles as sweeper guidance to avoid wrongly closing
    legitimate PRs.
- 'The Footprint Ladder' (core-tool decision): extend > CLI+skill > gated tool
  > plugin > MCP server in the catalog > new core tool (last resort).

Trim: 'Adding New Tools' intro points at the ladder. Detailed mechanics stay
where readers need them.
2026-06-09 21:31:07 -07:00
Austin Pickett
1770263ccc
fix(desktop): honor default project directory for new sessions (#43234)
* fix(desktop): honor default project directory for new sessions

The Settings picker persisted project-dir.json but the renderer kept
seeding new chats from sticky localStorage home. Prefer the configured
default on boot and session.create, pin TERMINAL_CWD at backend spawn,
and reject packaged install-dir paths that regressed after #37536.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(desktop): address review on default project dir PR

Add workspace cwd precedence tests, extract isPackagedInstallPath for
platform test coverage, and stop rewriting live $currentCwd when a
session is already active (cache-only until the next new chat).

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-09 23:28:59 -05:00
Brooklyn Nicholson
33a5bfa3c4 Merge remote-tracking branch 'origin/main' into bb/vscode-marketplace-themes
# Conflicts:
#	apps/desktop/electron/main.cjs
#	apps/desktop/src/app/command-palette/index.tsx
#	apps/desktop/src/themes/context.tsx
2026-06-09 23:22:36 -05:00
brooklyn!
8f73d0d945
feat(desktop): resizable VS Code-themed terminal pane + palette polish (#42521)
* refactor(desktop): dock terminal under chat and simplify file rail

Keep the right rail focused on file browsing while moving the persistent terminal into the chat column bottom slot, and make terminal colors follow the active light/dark mode instead of a fixed Solarized palette.

* fix(desktop): make the terminal a resizable, themed side pane

- Move the terminal into a resizable pane (viewport-% widths) that shares
  <main>'s stacking context, so its drag handle no longer sits under the
  fixed terminal overlay; works on either rail side.
- Restore +x on node-pty's spawn-helper before the first spawn to fix
  "posix_spawnp failed" on macOS prebuilds (real cause; drop the redundant
  shell-candidate retry loop).
- Gate terminal open/fit/start on document.fonts.ready and strip leading
  blank rows (re-armed before the resize Ctrl-L redraw) so the prompt sits
  flush at the top with no starship add_newline gap.
- Inherit the app editor-surface color as the terminal background.
- Bind Ctrl+` (⌃` on macOS) to toggle the terminal; add a palette entry.

* feat(desktop): show platform hotkey hints in the command palette

- Render each palette item's live binding as a <KbdGroup> hint via a new
  comboTokens() helper (mac shows ⌘/⌃/⌥/⇧, every other platform shows
  Ctrl/Alt/Shift — never a ⌘ on PC).
- Default the terminal toggle to ⌘` / Ctrl+` (the ~ key) on both platforms.
- Drop the hardcoded (⌘⏎) baked into the composer steer tooltip; render it
  platform-aware with formatCombo instead.

* fix(desktop): drop the active check on the command-palette terminal item

* fix(desktop): remove active/check states from the command palette

* fix(desktop): allow ⌥/Shift-drag selection over mouse-mode TUIs

Full-screen apps (hermes --tui, vim) enable mouse reporting, so a plain
drag can't select text and ⌘/Ctrl+L (add-selection-to-chat) had nothing
to send. Enable macOptionClickForcesSelection so ⌥-drag on macOS (Shift
elsewhere) forces a native selection over mouse-mode apps.

* feat(desktop): tell the in-pane agent it's embedded in the GUI

Set HERMES_DESKTOP_TERMINAL=1 on the terminal pane's shell env and surface
it in build_environment_hints, so a hermes/--tui launched inside the pane
knows it's next to the GUI chat and that ⌥/Shift-drag + ⌘/Ctrl+L sends a
selection to the composer. Distinct from HERMES_DESKTOP (agent backend).

* refactor(desktop): drop the redundant Ctrl+` terminal-toggle fallback

The toggle now ships as mod+` on both platforms, so the standard combo
index handles it — the bespoke fallback (and its stale 'old default'
comment) is dead weight.

* fix(desktop): read live terminal selection for ⌘/Ctrl+L

A redraw-heavy TUI (spinners/clocks) outruns onSelectionChange, leaving the
React selection state empty so the state-gated shortcut listener never
attached and ⌘L no-op'd. Always listen and read xterm's live selection (with
a native fallback) at press time; only swallow the key when there's text to
send. Drops the now-redundant custom key handler.

* feat(desktop): make any agent aware it's in the Hermes desktop GUI

Generalize the runtime-surface hint: fire for HERMES_DESKTOP (the backend
powering the GUI chat) as well as HERMES_DESKTOP_TERMINAL (a hermes in the
embedded terminal pane), so it's about being inside the desktop GUI, not
about being a TUI. The terminal-pane selection note stays pane-specific.

* feat(desktop): give the GUI agent a read_terminal tool

The in-app terminal buffer lives in the renderer (xterm), so expose it to the
chat agent over the same blocking bridge clarify uses: read_terminal emits
terminal.read.request, the renderer serializes the buffer (visible screen by
default, or a start_line/count range against total_lines) and answers
terminal.read.respond. Gated to the GUI via HERMES_DESKTOP.

Also restores the flipped-layout titlebar inset fix (app-shell +
desktop-controller) for terminal/preview rails at the window's left edge.

* chore(desktop): trim read_terminal comments

* feat(desktop): add a terminal toggle to the statusbar

The file rail lost its terminal icon, leaving ⌘` and the command palette
as the only ways in. Add a one-click toggle to the statusbar's left
cluster, mirroring the command-center item: it reads $terminalTakeover so
it lights up while the pane is open and stays in sync with the hotkey, and
is gated to chat view (the only place the pane can show).

* fix(desktop): relabel the terminal header button to what it does

The in-pane button claimed a focus/split fullscreen toggle ("Focus
terminal view" / "Return to split view", screen-full/normal icons), but
the terminal is just a resizable side pane — there's no fullscreen. The
button only mounts while the pane is open, so the focus branch was dead
and clicking it merely closed the terminal. Relabel to "Hide terminal"
with a close icon, drop the dead conditional and the now-unused takeover
read.

* fix(desktop): move the terminal toggle next to the version item

Relocate it from the left cluster to the right of the statusbar, just
left of the client version item.

* feat(desktop): default the terminal to PowerShell on Windows

Prefer pwsh (7+) then Windows PowerShell 5.1 over cmd.exe, falling back to
comspec only when neither is present. -NoLogo drops the startup banner so
the prompt sits flush like the POSIX shells.

* feat(desktop): show a persistent divider on the terminal pane

The resize sash only painted on hover, so the terminal/chat boundary was
invisible at rest. Add an opt-in `divider` prop to Pane that paints a thin
resting hairline on the resize edge (side-aware, so it tracks the rail when
the layout flips) and enable it on the terminal pane.

* refactor(desktop): resolve the terminal shell instead of hardcoding it

Make shell selection a real resolver: an explicit override wins
(HERMES_DESKTOP_SHELL on both platforms, $SHELL on POSIX), otherwise
auto-detect the best installed shell — pwsh > Windows PowerShell 5.1 > cmd
on Windows, zsh > bash > sh on POSIX. A shared shellSpecFor() picks the
interactive flags by family, so an overridden bash/pwsh/cmd all launch
correctly.

* fix(desktop): repaint the terminal on light/dark switch

Setting term.options.theme updated colors for the DOM renderer but not the
WebGL one, which caches glyph colors in a texture atlas — so already-drawn
cells kept their old palette after a mode switch. Hold the WebglAddon in a
ref and clear its atlas when the theme changes.

* fix(desktop): match the terminal palette to VS Code Light+/Dark+

Adopt VS Code's exact default ANSI palette (the terminalColorRegistry
defaults), enable minimumContrastRatio: 4.5 so foregrounds are clamped
against the background the way the integrated terminal does, and key the
light/dark choice off renderedMode (the painted surface) instead of
resolvedMode so it can't invert. The canvas + inset paint the live skin
surface (--ui-editor-surface-background) so the terminal blends with the
app and follows light/dark, while the contrast clamp keeps colors crisp.

* fix(desktop): tighten command palette search to substring matching

cmdk's default fuzzy scorer matched anything with the query letters
scattered across an item, so e.g. "color" never narrowed to color
entries. Add a substring filter: every typed word must literally appear
in an item's value/keywords, keeping results tight and predictable.

* fix(desktop): blend the terminal header into the skin surface

The persistent-terminal overlay painted the static palette background
(#1e1e1e/#ffffff), so the transparent header strip revealed a near-black
slab above the surface-colored body. Paint the overlay with the live
--ui-editor-surface-background so header and body read as one pane.

* fix(desktop): re-resolve the terminal surface on skin switch

The canvas surface only re-resolved on light/dark change, so switching
skins at the same mode left the WebGL canvas painted with the old tint
until reload. Key the resolve off themeName too. Also trim the palette
comments.

* chore(desktop): drop redundant terminal theming header comment
2026-06-09 23:15:20 -05:00
Brooklyn Nicholson
27a3211579 feat(desktop): install any VS Code theme from the Marketplace
Browse + install color themes from the VS Code Marketplace straight from
Cmd-K and Settings → Appearance. The Electron main process resolves the
extension, unzips the .vsix with a hand-rolled zip reader (zlib only, no
new deps), and hands back the raw theme JSON; the renderer converts it to
a DesktopTheme with a small seed → color-mix mapping.

- Folds an extension's light + dark variants into one theme family, so the
  light/dark toggle switches Solarized/GitHub variants and installing in
  dark mode stays dark.
- Guarantees accent contrast (WCAG AA) so imported sidebar labels read
  instead of vanishing into the surface.
- Filters icon/product-icon packs out of the Themes-category search.
- "Install theme…" lives atop the Cmd-K theme picker; imports fold into
  the Light/Dark groups by the modes they support.
2026-06-09 23:06:44 -05:00
Ben Barclay
5cf6e28a2f
fix(gateway): auto-start after container restart via planned-stop marker (#42675) (#43236)
* fix(gateway): auto-start after container restart via planned-stop marker

On Docker (s6-overlay), the gateway runs as a dynamically-registered s6
service. When the container stops/restarts/upgrades, s6 sends the gateway
a plain SIGTERM. The shutdown path (_stop_impl) ended with an
unconditional _update_runtime_status("stopped"), persisting
gateway_state=stopped to the volume. container_boot.py reads that on the
next boot and only auto-starts gateways whose last state was "running"
(_AUTOSTART_STATES) — so after a routine `docker compose up
--force-recreate` the gateway stays down and messaging channels silently
go dark, with no error surfaced (issue #42675).

The codebase already distinguishes intentional stops from unexpected
signals via the planned-stop marker (write_planned_stop_marker /
consume_planned_stop_marker_for_self): `hermes gateway stop`,
systemd/launchd ExecStop, and Ctrl+C write a marker before signalling,
so the handler classifies them as planned. An unmarked SIGTERM
(container/s6 restart, OOM, bare kill) is signal-initiated.

This wires that existing classification through to the state persist,
rather than adding unreliable signal-source inference:

- run.py: GatewayRunner._signal_initiated_shutdown, set in
  shutdown_signal_handler's unmarked-signal branch. In _stop_impl, a
  signal-initiated (non-restart) teardown now persists "running" instead
  of "stopped" — preserving the operator's run-intent and overwriting the
  mid-shutdown "draining" marker so _AUTOSTART_STATES matches on reboot.
  Operator stops and restarts persist "stopped" as before.

- service_manager.py: S6ServiceManager.stop() now writes the planned-stop
  marker for the supervised PID (read from s6-svstat) before `s6-svc -d`,
  so an in-container `hermes gateway stop` is correctly classified as
  intentional (parity with the systemd/launchd/host stop paths, which
  already mark). Best-effort: a marker-write failure falls back to the
  safe signal-initiated path.

Tests: shutdown persist-decision table (signal→running, operator→stopped,
restart→stopped), s6 stop marker write + svstat PID parse + failure
tolerance. The signal→running and s6-marker tests fail without the
respective source change. Verified end-to-end against a container built
from this branch: an unmarked SIGTERM to the live gateway leaves
gateway_state=running (shutdown-context log confirms signal path);
existing real container-restart suite still green.

* docs(docker): clarify gateway autostart distinguishes operator-stop from container-kill

The per-profile-supervision section described the autostart-across-restart
contract as "running gateways come back, stopped stay stopped" without
spelling out what records 'stopped'. That contract was the source of
#42675 confusion: users expected a restart to bring the gateway back and
it didn't. With the write-side fix, only an explicit `hermes gateway stop`
records 'stopped'; container/s6 restart SIGTERMs (incl. image upgrades and
unexpected exits) leave the state 'running' so the gateway auto-starts.
Make that distinction explicit in both the multi-profile and
per-profile-supervision sections.

* test(docker): real-restart autostart E2E for #42675

Adds test_live_gateway_autostarts_after_real_restart_without_manual_state_stamp:
a live s6-supervised gateway is killed by an actual `docker restart`
SIGTERM (no manual gateway_state stamp, no planned-stop marker) and must
auto-start on the next boot. Exercises the WRITE side of the fix that the
existing stamp-based tests bypass.

Verified to FAIL against an origin/main image (reconciler logs
prior_state=stopped action=registered — the #42675 bug) and PASS against
the fixed image (prior_state=running action=started).
2026-06-10 14:01:34 +10:00
Siddharth Balyan
b4170f3ac2
fix(cron): don't strict-scan script-injected output in no-skills jobs (#43223)
The runtime assembled-prompt scan (#3968 lineage) selected its pattern
tier on has_skills alone. A script-driven, no-skills job injects its
script's stdout into the prompt, and that blob was scanned with the
STRICT user-prompt pattern set — so any command-shape string in the
data feed (e.g. a triage bot ingesting a bug report that quotes
`rm -rf /`) hard-blocked the job on every tick.

Script output and context_from output are runtime DATA produced by
operator-authored code — the same trust class as install-vetted skill
markdown, not a user-authored directive prompt. Select the scan tier by
what the assembled prompt CONTAINS: when it includes skill content OR
injected data, use the looser _scan_cron_skill_assembled set (keeps
unambiguous injection directives, drops command-shape patterns,
sanitizes invisible unicode instead of blocking).

Defense-in-depth is preserved:
- The raw user prompt is still strict-scanned at create/update
  (api_server paths untouched) AND re-scanned strict at runtime even
  when the looser tier was selected for the data blob.
- Plain no-script/no-skills jobs keep the strict scan on the whole
  assembled prompt.
- Injection directives arriving via script stdout still block.

Rejected alternative: removing destructive_root_rm from the strict set
or a per-job skip_injection_scan flag — both weaken the guard globally.
2026-06-10 08:27:24 +05:30
Ben Barclay
7df3aa34b1
fix(dashboard-auth): warn when public_url override is silently rejected (#43214)
A non-empty HERMES_DASHBOARD_PUBLIC_URL / dashboard.public_url value that
fails URL validation (overwhelmingly: a missing http(s):// scheme, e.g.
"hermes.domain.com") was silently discarded by resolve_public_url(),
falling back to reconstructing the OAuth redirect_uri from request
headers. Behind a reverse proxy that doesn't forward X-Forwarded-Proto
reliably, that yields an http:// callback even though the operator
explicitly set the public URL — with no signal as to why (#42780).

Emit a deduplicated operator-facing WARNING (once per distinct value,
since resolve_public_url runs per request) naming the offending value
and the required scheme. Turns a silent footgun into a self-diagnosing
one; behaviour is otherwise unchanged.

Tests assert the warning fires for a scheme-less value, is deduplicated
across repeated calls, and stays silent for a valid value — all three
fail without the fix.
2026-06-10 12:14:57 +10:00
brooklyn!
b96bd4808d
feat(desktop): open any chat in its own window (#43219)
Pops a session into a standalone, focused window for side-by-side work.
A secondary window loads the renderer at the session route with a
?win=secondary flag (ahead of the HashRouter '#'); it drops the global
sidebar plus the install/onboarding overlays and renders a single chat,
sharing the one local gateway over WS (no backend duplication). The main
process keys windows by sessionId so re-opening focuses the existing one
and self-cleans on close.

Open it via:
- ⌘-click (mac) / ⌃-click (win/linux) a sidebar session — the universal
  "open in new window" gesture. Archive moves to the ⋯ / right-click menus
  only, off the easy-to-misfire modifier-click.
- "New window" in the session ⋯ and context menus (link-external icon,
  i18n'd across en/ja/zh/zh-hant).

A standalone window has no left rail, so AppShell treats its edge as
uncovered and applies the titlebar inset — the chat title clears the
macOS traffic lights instead of hiding behind them.

Co-authored-by: tim404x <tim404x@users.noreply.github.com>
2026-06-09 21:09:45 -05:00
Ben Barclay
d33965396e
feat(tui): include session name in the terminal titlebar (#43188)
The terminal/console titlebar was composed from status marker + model +
cwd only; the session's (auto-)title never appeared, even though the TUI
already knows it.

Change the format to `<marker> <session name> · <model> · <cwd>`, with the
session name and cwd each omitted when absent so single-segment titles stay
clean. The current session's live title is pulled from the existing
session.active_list poll (which already carries each session's current flag
and title), so there's no extra round-trip; UiState gains a sessionTitle
field updated only when it actually changes, preserving the existing
idle-flicker guard.

Extract the join logic into a pure composeTabTitle() helper in domain/paths
and cover its edge cases (name omitted, cwd omitted, whitespace-only name,
marker-only fallback, truncation, boundary length) in paths.test.ts.
2026-06-10 11:24:01 +10:00
Gille
258d24039f
fix(desktop): scope thinking disclosure pending state (#43197) 2026-06-09 20:16:20 -05:00
brooklyn!
ab5f1a1f11
feat(desktop): Mac-style session switcher (^Tab / ^⇧Tab / ^1-9) (#43111)
Bind session.next/prev to Control+Tab / Control+Shift+Tab with a distinct
`ctrl` modifier token (literal Control on macOS — not Cmd, which the OS
reserves). Add ^1…^9 positional jumps mirroring profile ⌘1…⌘9.

Mac-style interaction:
- Quick ^Tab tap jumps on keydown with no HUD (even if Ctrl stays down)
- Hold Tab ~220ms, or tap Tab again while Ctrl is held → compact HUD
- Ctrl↑ commits the highlight; Esc cancels; rows clickable (^+click safe)
- Recency-ordered list snapshotted on open; cycles by stored session id

Includes combo.test.ts + session-switcher.test.ts.
2026-06-09 20:12:46 -05:00
brooklyn!
8bb6529553
fix(desktop): sidebar sections never overlap — two-mode CSS scroll + collapse/cap groups (#43147)
* fix(desktop): prevent sidebar section overlap

Use a shared sidebar section scroller only on short windows so sections do not overlap, while preserving per-section scrolling on taller layouts.

* fix(desktop): measure section stack for compact sidebar mode

Window-height media query kept big windows in compact mode whenever the OS chrome ate into 830px; observe the section stack element instead so compact only engages when the stack is actually short.

* refactor(desktop): drive sidebar compact mode with CSS, not JS

Replace the matchMedia hook with a `short` (max-height: 830px) Tailwind
variant so the per-section scrollers flatten into one shared scroll stack on
short windows purely in CSS. Taller windows keep their per-group scrollers and
recents virtualization unchanged.

* refactor(desktop): pure-CSS two-mode sidebar scroll + collapse/cap groups

Drop the JS-measured compaction in favour of a single `compact` height
variant (max-height: 768px):
- tall: every section is its own capped, independent scroller; Sessions
  is the lone flex-1 scroller.
- short: sections flatten and the stack scrolls as one.

Every section is now `shrink-0`, so nothing is squeezed below its
content and bled onto a sibling — the root cause of the header overlap
(flexbox implied min-size). Sessions keeps its virtualized scroller in
short mode only when it's the long list.

Non-session groups (messaging, cron) collapse by default — expanded ids
persist per platform — and render 3 rows, revealing 10 more on demand.
Extract the shared SidebarLoadMoreRow. Stress harness seeds 50 recents
to mirror the real first page.

* chore(desktop): trim sidebar comments, unify "compact" naming

Self-review polish: condense the over-long mode comments, use "compact"
consistently (matching the variant) instead of mixing "short", and drop a
no-op useCallback around revealMoreMessaging.

* chore(desktop): drop dev sidebar stress harness from the PR

Remove stress-probe.ts and its main.tsx import — it was a throwaway
testing aid, not something to ship.
2026-06-10 01:11:45 +00:00
BROCCOLO1D
29036155ce
fix(terminal): lazy-parse docker env config (#42733)
Co-authored-by: BROCCOLO1D <279959838+BROCCOLO1D@users.noreply.github.com>
2026-06-10 11:04:27 +10:00
xxxigm
8b84d82227
fix(desktop): send on Enter from live editor text, not stale composer state (#39639)
* fix(desktop): send on Enter from live editor text, not stale composer state

Pressing Enter often did nothing (~90% with IME / fast typing); adding a
trailing space "fixed" it. The composer's submit path read the draft from the
AUI composer state (`useAuiState(s => s.composer.text)`) and the derived
`hasComposerPayload`, both of which lag the contentEditable DOM by a render. On
fast typing or IME composition the final keystroke(s) weren't in state yet, so
`submitDraft()` saw an empty draft and dropped the message. A trailing space
only worked around it by forcing an extra input event that flushed the state.

submitDraft() now refreshes draftRef from the editor node and submits/queues
based on the live DOM text, and the Enter handler decides the queue-drain vs
submit branch from the DOM too. draftRef is already synced on every input
event, so this just closes the in-flight-keystroke gap.

Fixes #39630. Also addresses the "typing + Enter does nothing" reports in

#39623.

* test(desktop): cover Enter-submit from live editor text (#39630)

Pin the contract that the composer's Enter path reads the live DOM editor
text, not the render-lagged composer state: a just-typed message sends even
when state hasn't synced; while busy it queues (never drains the queue or
cancels); an empty Enter while busy is a no-op; and an empty idle Enter
drains the next queued prompt. Faithful DOM-event repro mirroring
handleEditorKeyDown + submitDraft.
2026-06-10 00:51:23 +00:00
xxxigm
93340fa3c1
fix(tui_gateway): honor target profile's terminal.cwd on desktop profile switch (#40892)
* fix(tui_gateway): honor target profile's terminal.cwd on desktop profile switch

The desktop's app-global remote mode serves every profile from one
tui_gateway backend, so the process-global TERMINAL_CWD only reflects the
launch profile. After switching profiles, a new session resolved its
workspace from that stale env var and inherited the previous profile's
directory.

Add _profile_configured_cwd() to read a non-launch profile's own
terminal.cwd from its config.yaml (skipping placeholder/empty/missing and
non-existent paths so callers fall back cleanly), and wire it into
_completion_cwd() with precedence: explicit client cwd -> existing session
cwd -> bound profile's configured cwd -> TERMINAL_CWD -> os.getcwd().

Fixes #40334

* test(tui_gateway): cover per-profile cwd resolution (#40334)

Pin the new contract: _profile_configured_cwd reads a profile's own
terminal.cwd and rejects placeholders/missing paths, and _completion_cwd
prefers a bound profile's cwd over a stale launch-profile TERMINAL_CWD
while still letting an explicit client cwd win.
2026-06-09 19:45:29 -05:00
xxxigm
59ea2f98e6
fix(desktop): always show the Manage-profiles overflow (#42871)
The "..." overflow that opens the profile manager (the only UI to edit a
profile's SOUL.md) was gated behind profiles.length > 1, so a user with
only the default profile couldn't edit its persona without first creating
a throwaway second profile. Render it unconditionally.
2026-06-09 19:32:25 -05:00
brooklyn!
aecdacb11b
Merge pull request #43109 from NousResearch/fix/desktop-remote-attach-drops
fix(desktop): stage dropped files into the remote session workspace
2026-06-09 19:22:11 -05:00
Brooklyn Nicholson
7ffc216bc0 fix(agent): make a binary @file: reference actionable instead of a dead end
A binary @file: ref (PDF, docx, spreadsheet, …) expanded to a bare
"binary files are not supported" warning with no content. The model saw a
failure and gave up — e.g. a dropped PDF came back as a text note claiming the
type was unsupported, even though the file was staged on disk right next to it.

Inject an actionable content block instead: the path, mime type, size, and a
nudge to use its tools to read/convert/view the file (and explicitly not to tell
the user the type is unsupported). General across every binary type — not
PDF-specific. The file already resolves where the agent's tools run (local cwd
or the staged copy in a remote session workspace), so it can act on it directly.
2026-06-09 19:16:46 -05:00
brooklyn!
218452b050
fix(state.db): recover from malformed sqlite_master so hidden sessions reappear (#43149)
* fix(state.db): recover from malformed sqlite_master so hidden sessions reappear

The corruption class behind "Desktop/Dashboard show no sessions while
hundreds of session files sit on disk" is a malformed sqlite_master — most
often a duplicate object row, e.g. two CREATE VIRTUAL TABLE messages_fts
entries — surfacing as:

    sqlite3.DatabaseError: malformed database schema (messages_fts) -
    table messages_fts already exists

SQLite parses the whole schema while preparing the FIRST statement on a
connection, so on this class every statement fails before it runs: PRAGMA
journal_mode (which is where SessionDB.__init__ actually trips, in
apply_wal_with_fallback, BEFORE _init_schema), PRAGMA integrity_check, and
even DROP TABLE. The only operations that still work are
PRAGMA writable_schema=ON plus direct sqlite_master surgery. A plain
FTS-index rebuild at the _init_schema layer therefore cannot reach or fix
this; the canonical sessions/messages rows are intact — only the derived
schema is broken.

Add a dedicated recovery that operates where the failure actually happens:

- hermes_state.repair_state_db_schema(): backs up the raw file first, then a
  least-destructive ladder — (1) de-duplicate sqlite_master keeping the
  lowest rowid per object (preserves the existing FTS index), escalating to
  (2) drop every messages_fts* schema object + VACUUM and let the next open
  rebuild the FTS index from messages. sessions/messages are never modified.
  Plus is_malformed_db_error() to discriminate this class.
- SessionDB.__init__ auto-heals: on a malformed-schema open error it repairs
  once (process-guarded against loops / concurrent web_server opens) and
  reopens, so Desktop/Dashboard recover on their own instead of silently
  showing "no sessions".
- hermes doctor --fix detects the malformed class and repairs it (reporting
  the recovered session count + backup name).
- hermes sessions repair [--check-only] [--no-backup] runs on the raw file
  path, since SessionDB() itself cannot open a malformed DB.

Supersedes #32589 and #33869: both targeted FTS corruption but gated their
repair behind statements (integrity_check / SELECT / DROP TABLE) that
themselves fail on this class, and neither addressed the apply_wal_with_fallback
open-time failure. Credit preserved via Co-authored-by.

Closes #33865.

Co-authored-by: João Vitor Cunha <145560011+plcunha@users.noreply.github.com>
Co-authored-by: Tuna Dev <273476039+tuancookiez-hub@users.noreply.github.com>

* test(state.db): cover strat-B escalation + unrepairable safe-fail paths

---------

Co-authored-by: João Vitor Cunha <145560011+plcunha@users.noreply.github.com>
Co-authored-by: Tuna Dev <273476039+tuancookiez-hub@users.noreply.github.com>
2026-06-09 18:49:08 -05:00
Brooklyn Nicholson
29147afd63 fix(desktop): friendlier toast when a remote attachment exceeds the 16MB cap
Remote attachments read their bytes through the readFileDataUrl IPC, which is
hard-capped at 16MB and rejects with a raw "file is too large (N bytes; limit M
bytes)" string straight into the failure toast (helix4u review note on #43109).

Translate that into "<file> is too large to upload to the remote gateway (max
16 MB)", parsing the limit out of the message so it tracks the real cap. Applies
to both the image and non-image remote read paths; non-cap errors pass through
unchanged. Adds unit coverage for both.
2026-06-09 18:31:09 -05:00
Brooklyn Nicholson
b021497bc8 fix(desktop): show a staging spinner in the edit composer while OS drops upload
The message-edit composer staged dropped OS files asynchronously with no
visible state, so confirming the edit before the upload resolved could send
the message without the gateway-side ref (helix4u review note on #43109).

Add a staging flag: while uploadOsDropRefs is in flight, show a small spinner
pill in the bubble and block submit (disabled send button + submitEdit guard)
so the edit can't outrace the ref insertion. New `attachingFile` i18n string
across en/zh/zh-hant/ja.
2026-06-09 18:26:54 -05:00
Brooklyn Nicholson
891c9a6823 fix(desktop): close eager-upload races flagged in review
Two races in the drop-time eager upload:

- Resurrected chip: the success path used addComposerAttachment, which
  re-appends when the id is gone, so a file removed mid-upload reappeared once
  the upload resolved. Add updateComposerAttachment (update-only; no-op when the
  chip was removed) and use it on both the eager success path and submit-time
  sync.
- Duplicate upload: submit-time sync didn't join an eager upload still in
  flight, so drop-then-Enter could fire file.attach twice and leave a duplicate
  under .hermes/desktop-attachments/. Track in-flight eager uploads by id and
  await the pending one before deciding to re-upload, reusing its gateway ref.

Tests: composer-store no-resurrect unit tests + a join-on-submit integration
test asserting a single file.attach.

Addresses @helix4u review on #43109.
2026-06-09 18:21:10 -05:00
kshitijk4poor
72154ad879 perf(ci): cache uv + use uv sync in tests workflow
Both jobs in tests.yml (`test` matrix and `e2e`) start from a cold uv
cache on every run and install deps with `uv pip install -e ".[all,dev]"`,
which re-resolves pyproject.toml ranges and rebuilds the editable install
each time.

Two changes:

1. Enable uv's official CI caching via setup-uv's `enable-cache: true`,
   keyed on pyproject.toml + uv.lock, plus `uv cache prune --ci` to keep
   the persisted cache small. Warm runs install from cache instead of
   re-downloading/building wheels.

2. Replace the manual `uv venv` + `uv pip install -e` with
   `uv sync --locked --python 3.11 --extra all --extra dev`. sync installs
   the exact pinned set from uv.lock (and fails if the lock is stale vs
   pyproject.toml), creating .venv itself. This is reproducible and, with a
   warm cache, measurably faster than the editable pip install (~3-4x on the
   steady-state install step locally). Downstream steps keep using
   `source .venv/bin/activate`; sync writes .venv to the same path.

Follows the Astral-recommended pattern for uv in GitHub Actions:
https://docs.astral.sh/uv/guides/integration/github/

Co-authored-by: Wesley Simplicio <wesleysimplicio@live.com>
2026-06-09 18:30:44 -04:00
Brooklyn Nicholson
153060e206 fix(desktop): render optimistic image thumbnails from in-hand base64
The in-flight user bubble seeded image attachment refs as `@image:<localpath>`.
In remote-gateway mode that path lives on the desktop, not the gateway, so the
inline thumbnail fetch hit /api/media and 403'd ("Path outside media roots"),
flashing a fallback chip until submit uploaded the bytes.

Seed (and keep) image refs as the raw base64 preview data URL instead. It
renders inline via extractEmbeddedImages with zero network, and survives the
post-sync rewrite (the agent gets the bytes through the attached-image pipeline,
not this display ref) so the thumbnail no longer remounts/flashes. Non-image
refs are unchanged.

Adds optimisticAttachmentRef + unit coverage.
2026-06-09 17:03:42 -05:00
Brooklyn Nicholson
4906dcfc25 fix(desktop): stage dropped files into the remote session workspace
Finder/OS drops became `@file:/Users/...` refs that only resolve when the
gateway shares the local disk, so on a remote gateway non-image files
(PDF/CSV/Markdown/...) never reached the agent. Route OS drops through the
file.attach / image.attach_bytes upload pipeline — in-app project-tree and
gutter drags stay inline workspace-relative refs — across every drop surface:
the conversation area, the composer form, the contenteditable input, and the
message-edit composer (which still reproduced the bug).

Also:
- upload dropped files eagerly when a session exists, so the card shows a
  spinner instead of stalling the send (images stay submit-time to avoid
  racing their thumbnail write);
- round the attachment card and drop the monospace detail;
- render image previews from the bytes we already hold, so a pasted/dropped
  screenshot shows its thumbnail and previews even when its only on-disk copy
  is a transient path (the data URL is not persisted to localStorage).

Supersedes #38615, #41203.

Co-authored-by: LeonSGP <154585401+LeonSGP43@users.noreply.github.com>
Co-authored-by: Teknium <127238744+teknium1@users.noreply.github.com>
2026-06-09 16:50:08 -05:00
Teknium
57c6714995
fix(models): keep curated Anthropic aliases in /model picker (#43103)
The Anthropic picker returned the live /v1/models dump verbatim whenever
credentials were configured. Anthropic's API lags newly-routed curated
aliases (e.g. claude-fable-5, reachable on Anthropic before the models
endpoint enumerates it), so the curated entry vanished from the picker.

Merge curated _PROVIDER_MODELS["anthropic"] with the live catalog —
curated first, live-only appended, deduped — mirroring the OpenAI
curated-merge path. Live failure / no creds falls back to curated verbatim.
2026-06-09 14:45:19 -07:00
ethernet
a5d05cf30e fix(nix); don't run .#fix-lockfiles
its so slow
2026-06-09 16:55:33 -04:00
ethernet
68a997fed4 add website links to readme for seo 2026-06-09 16:35:34 -04:00
Jeffrey Quesnelle
49dd776d8b
Merge pull request #43041 from NousResearch/fix/fable-anthropic
add Fable 5 to model list for Anthropic provider
2026-06-09 15:38:51 -04:00
emozilla
d7886da08c add Fable 5 to model list for Anthropic provider 2026-06-09 15:33:42 -04:00
xxxigm
02f878ec5a
docs(windows): correct native data dir to %LOCALAPPDATA%\hermes (#42856)
* docs(windows): correct native data dir to %LOCALAPPDATA%\hermes

The Windows-native guide claimed a deliberate split where config, auth,
skills, and sessions live under %USERPROFILE%\.hermes. That is not what
the installer does: scripts/install.ps1 sets HERMES_HOME=%LOCALAPPDATA%\hermes,
so data actually lives in %LOCALAPPDATA%\hermes alongside the disposable
install (the hermes-agent\, git\, node\, bin\ subdirectories) — `hermes
config` confirms config.yaml/.env resolve there, not under %USERPROFILE%.

Update the data-layout table, the "split is deliberate" note, the env-var
and uninstall sections to describe the real layout: data and install share
the %LOCALAPPDATA%\hermes root, reinstall only replaces hermes-agent\, and
a full wipe targets %LOCALAPPDATA%\hermes (with %USERPROFILE%\.hermes kept
only as a legacy/WSL cleanup). Mention HERMES_HOME as the override knob.

* docs(windows): fix PATH + bin layout to match installer

The installer adds hermes-agent\venv\Scripts (where hermes.exe lives) to
User PATH and sets HERMES_HOME — not %LOCALAPPDATA%\hermes\bin. The \bin
dir holds Hermes's managed uv.exe, not a hermes.cmd shim. Correct the
install-step list and the data-layout table accordingly.

* fix(install): show real HERMES_HOME path in setup messages

The native Windows installer wrote config/env/skills under $HermesHome
(%LOCALAPPDATA%\hermes) but its success messages claimed ~/.hermes,
which doesn't exist on native Windows. Print the actual paths so a new
user can find their config, .env, and skills.
2026-06-09 14:11:20 -05:00
brooklyn!
8d71c38919
fix(desktop): rebind sessions after websocket reconnect (salvage of #41740) (#43004)
* fix(desktop): rebind sessions after websocket reconnect

* docs(desktop): explain the reconnect-resume guard in use-route-resume

The reconnect fix turns on two subtle conditions with no inline rationale:
`seenGatewayStateRef` suppresses a spurious "became open" on the first effect
run (so a session mounting with the gateway already open doesn't double-resume),
and the `gatewayBecameOpen ||` arm forces a re-resume even when the route looks
`alreadyActive` because the cached runtime id can be stale after the gateway
rebinds/reaps the session. Comment both so the next reader doesn't "simplify"
them back into the original bug. No behavior change.

---------

Co-authored-by: Josh Dow <josh.dow@prepad.io>
2026-06-09 19:01:00 +00:00
Siddharth Balyan
46fedef07f
fix(openrouter): never send reasoning field for adaptive Anthropic models (#43012)
The previous fix (#42991) only omitted reasoning when it was being disabled.
But reasoning-mandatory Anthropic models (Claude 4.6+, fable) 400 with
thinking.type.disabled on EVERY tool-continuation turn even when reasoning is
enabled: chat_completions never replays signed thinking blocks, so the prior
assistant tool_call has no thinking, and OpenRouter resolves "reasoning
requested but history has none" by emitting thinking.type.disabled — which
these models reject. Result: first turn works, every turn after the first tool
call dies (HTTP 400, non-retryable).

OpenRouter ignores reasoning.effort for adaptive Anthropic models anyway (the
model self-decides), so the reasoning field is pointless for them on every turn
and harmful on tool-replay turns. Omit it entirely → adaptive default.

- openrouter profile: drop the reasoning field for reasoning-mandatory Anthropic
  models regardless of enabled/disabled; legacy Anthropic + non-Anthropic models
  unchanged.
- tests: assert omission across enabled/disabled/effort variants; parity tests
  switched to a non-Anthropic reasoning model (deepseek) since Anthropic 4.6+ no
  longer carries a reasoning field.

Verified live end-to-end: a tool-replay turn on anthropic/claude-fable-5 with
reasoning enabled now builds extra_body=None and returns HTTP 200 (was 400).
2026-06-10 00:18:23 +05:30