Widen the tool-call section label column for text-xs + text-display, and
restore normal-case on the terminal copy button so it stays sentence case.
Co-authored-by: Cursor <cursoragent@cursor.com>
Restore pre-typography-refactor brand classes and explicitly opt out of
uppercase so the logo stays "Hermes Agent", not HERMES.
Co-authored-by: Cursor <cursoragent@cursor.com>
Restore main-style Typography on the brand title (Collapse via font-sans),
without font-mondwest or forced uppercase on the logo.
Co-authored-by: Cursor <cursoragent@cursor.com>
Replace Typography on Hermes Agent labels with a plain span using
font-mondwest + uppercase (matching main via shell inheritance). Typography
was injecting font-sans and fighting font-mondwest; text-display was not
the pre-refactor brand style.
Co-authored-by: Cursor <cursoragent@cursor.com>
text-display alone is correct in source but stale web_dist or missing DS
CSS leaves nav looking title-cased; explicit uppercase restores brand
chrome. Document Vite vs dashboard URL in web README.
Co-authored-by: Cursor <cursoragent@cursor.com>
Drop the Typography mondwest prop on sidebar/mobile brand labels; it
forced tracking-[0.1875rem] instead of the original tight brand tracking.
Use font-mondwest via className like main did via shell inheritance.
Co-authored-by: Cursor <cursoragent@cursor.com>
Page title h1 and mobile/sidebar brand labels now opt into the DS
text-display utility (with Mondwest on brand wordmarks) so chrome stays
uppercase after removing the global App root transform.
Co-authored-by: Cursor <cursoragent@cursor.com>
Register @font-face rules for Mondwest, Collapse, and Rules so DS
components load brand fonts after the 0.14.x split from globals.css.
Also merged main to sync the branch.
Co-authored-by: Cursor <cursoragent@cursor.com>
Adds a Termux runtime detection helper and gates three TUI defaults on it:
- Skip the startup scrollback clear on Termux so users can review/copy
earlier output after reopening the app. Desktop keeps the existing
\x1b[2J\x1b[H\x1b[3J slate (AlternateScreen takes over there anyway).
- Default INLINE_MODE on under Termux: primary-buffer rendering makes
long-thread review and copy/paste much less fragile when users
background/foreground the app. Override with HERMES_TUI_INLINE=0/1.
- Default mouse tracking off under Termux so touch selection isn't
intercepted by terminal mouse protocols. Explicit override via
HERMES_TUI_MOUSE_TRACKING=0/1; legacy HERMES_TUI_DISABLE_MOUSE still
works on desktop.
Detection is purely env-based (TERMUX_VERSION or PREFIX path) with an
explicit opt-out HERMES_TUI_TERMUX_MODE=0 for debugging. Non-Termux
platforms keep every existing default.
Co-authored-by: adybag14-cyber <252811164+adybag14-cyber@users.noreply.github.com>
Drop mondwest on version footer (mono-ui only) and opt the mobile model/tools
sheet title into text-display after removing global uppercase.
Co-authored-by: Cursor <cursoragent@cursor.com>
Clarify that raw uppercase is legacy-only (prefer text-display for new
code) and broaden when normal-case is appropriate on DS buttons.
Co-authored-by: Cursor <cursoragent@cursor.com>
Linux/macOS CI runners don't have ctypes.windll, so the elevated-gateway
test fails at module load. Adding raising=False lets monkeypatch install
the mock attribute without first requiring it to exist.
Preserve Windows profile install decisions across UAC handoff, avoid visible console windows by launching via pythonw, make repeated install/start idempotent, recreate stale Scheduled Tasks, and separate start-now from login auto-start behavior. Add Windows gateway regression coverage and systemd setup tests for the shared install flow.
Apply Windows CREATE_NO_WINDOW flags to foreground local terminal subprocesses and tracked background processes so Hermes operations do not flash or steal focus with extra console windows.
Apply CREATE_NO_WINDOW flags when the cron scheduler launches job scripts on Windows so gateway-managed no-agent cron jobs do not flash cmd or python console windows every tick.
* fix(update): detect concurrent hermes.exe on Windows; retry + restart-defer quarantine
Closes#26670.
When 'hermes update' runs on Windows with another hermes.exe alive (most
commonly the Hermes Desktop Electron app's spawned backend) _quarantine_running_hermes_exe()
fails to rename the venv shim with [WinError 32]. uv pip install -e .
then exits 2, the git-pull fast path is silently abandoned, and the ZIP
fallback runs (and fails the same way) before eventually succeeding.
This change implements three of the five proposed fixes from the issue:
1. Concurrent-instance detection (preferred fix). _detect_concurrent_hermes_instances()
uses psutil to enumerate processes whose .exe is one of our venv shims
(hermes.exe / hermes-gateway.exe), excluding the caller's PID. When any
match exists, cmd_update prints an actionable message naming the
blocking PIDs and exits 2 BEFORE any destructive work. New --force flag
bypasses the gate.
2. Retry + restart-deferred fallback. _quarantine_running_hermes_exe()
now retries the rename up to 4 times with 100/250/500/1000 ms backoff
(covers the transient AV-scanner-handle case). If all retries fail,
it schedules the replacement via MoveFileExW with the OS deferred-rename
flag so the new shim can land at the original path and the update
completes; the old image is fully unloaded after the user's next
system restart.
3. Actionable warning text. The old 'Could not quarantine: [WinError 32]'
warning is replaced with one that names the likely culprits (Hermes
Desktop, REPLs, gateway, AV) and points to the new --force flag.
Tests:
- 13 new tests in tests/hermes_cli/test_update_concurrent_quarantine.py
covering: psutil-based enumeration, self-pid exclusion, case-insensitive
matching of .EXE, no-psutil graceful degradation, off-Windows no-op,
helpful warning formatting, retry-then-succeed, restart-deferred fallback,
cmd_update abort + exit code 2, and --force bypass.
- New autouse fixture in tests/hermes_cli/conftest.py defaults
_detect_concurrent_hermes_instances to [] so the rest of the suite
isn't tripped by the developer's own running hermes.exe. Opt-out marker
'real_concurrent_gate' registered in pyproject.toml.
- Updating docs page (website/docs/getting-started/updating.md) gains a
short section explaining the new Windows error and remediation.
* chore: refresh uv.lock to match pyproject.toml exact pins
aiohttp 3.13.4 -> 3.13.3 (matches pyproject pin: aiohttp==3.13.3)
anthropic 0.87.0 -> 0.86.0 (matches pyproject pin: anthropic==0.86.0)
hermes-agent 0.13.0 -> 0.14.0 (matches pyproject version)
CI's uv lock --check was failing on the merged state because main
drifted: pyproject.toml uses exact == pins for those two deps and the
hermes-agent version was bumped to 0.14.0 but the lockfile still had
0.13.0.
Why
- Whole dashboard was force-uppercased by a single \`uppercase\` on the
App.tsx root, which inherited into every page and forced ~23
\`normal-case\` opt-outs across 7 files just to keep dynamic content
(model names, theme names, etc.) readable.
- Micro-typography (\`text-[0.55rem]\` / \`text-[0.6rem]\` / \`text-[9px]\` /
\`text-[10px]\` / \`text-[11px]\`) combined with stacked alpha
(\`text-muted-foreground/60\` over a 55%-alpha base, \`opacity-30\` on
nav headers) produced text that fails WCAG AA at small sizes.
- Per-theme \`--theme-font-sans\` was being clobbered by a hard-coded
\`font-mondwest\` on the App.tsx root.
Changes
- Drop global \`uppercase\` + \`font-mondwest\` from the App.tsx root; default
to \`text-text-primary\` so body content inherits the theme font.
- Map \`--color-muted-foreground\` to \`--color-text-secondary\` so the
long tail of \`text-muted-foreground\` call sites get a WCAG-AA-targeted
color instead of 55%-alpha midground.
- Apply the new DS \`text-display\` utility on intentional brand chrome
(sidebar nav section labels, page titles, mobile header brand,
segmented filters, badges, ChatSidebar headings).
- Remove the 23 \`normal-case\` opt-outs that only existed to fight the
global \`uppercase\`. Retain \`normal-case\` on the 4 DS \`Button\`
instances that legitimately display dynamic content.
- Bump every \`text-[0.55-0.7rem]\` / \`text-[9-11px]\` to \`text-xs\` (12px
floor) across PluginsPage, ConfigPage, SkillsPage, ModelsPage,
SessionsPage, AnalyticsPage, LogsPage, EnvPage, ChatPage,
ChatSidebar, ToolCall, ModelInfoCard, ModelPickerDialog,
SidebarStatusStrip, SidebarFooter, OAuthProvidersCard, SlashPopover,
ThemeSwitcher, LanguageSwitcher, BottomPickSheet, AutoField.
- Replace stacked-alpha refs (\`text-muted-foreground/60\`,
\`text-midground/70\`, \`opacity-30/50/60\` on text) with semantic tokens
(\`text-text-secondary\`, \`text-text-tertiary\`, \`text-text-disabled\`).
- Bump \`@nous-research/ui\` to 0.16.0 (which adds the \`text-display\`
utility and semantic text tokens this PR depends on).
- Add a Typography & contrast rules section to \`web/README.md\` codifying
the 12px text floor, 0.7 opacity floor on text, "uppercase via
text-display only" rule, and "prefer semantic tokens" guideline so
the dashboard doesn't drift back.
This pairs with NousResearch/design-language#22 which provides the
\`text-display\` utility and semantic text tokens.
Co-authored-by: Cursor <cursoragent@cursor.com>
When discord.py is not installed at import time, DISCORD_AVAILABLE=False
and the view class definitions at module bottom are skipped.
check_discord_requirements() performs a lazy install and sets
DISCORD_AVAILABLE=True but never re-ran the class definitions, causing
NameError on the first button interaction (exec approval, slash confirm, etc.).
Extract the five ui.View subclasses into _define_discord_view_classes() and
call it both at module load (when discord.py is pre-installed) and inside
check_discord_requirements() after a successful lazy install.
Extends the previous commit to cover the remaining additive-column index
that sits on the same migration trap:
- ``task_events.run_id`` -> ``idx_events_run`` was still in SCHEMA_SQL.
A legacy ``task_events`` table predating #17805 (no ``run_id``) would
still abort ``executescript`` before ``_migrate_add_optional_columns``
could add the column. Hoisted out of SCHEMA_SQL and made unconditional
in the migration alongside the other three indexes.
- Removed the now-redundant ``CREATE INDEX idx_tasks_idempotency`` that
was nested inside the ``if "idempotency_key" not in cols`` branch.
The unconditional create lower in the function makes it idempotent
on both fresh and legacy DBs.
- Strengthened the regression test to cover all four indexes
(``idx_tasks_session_id``, ``idx_tasks_tenant``, ``idx_tasks_idempotency``,
``idx_events_run``) and to seed a pre-#17805 ``task_events`` shape that
exercises the ``run_id`` migration path.
The result: every ``CREATE INDEX`` that depends on an additive column now
runs after the migration ensures the column exists. Verified against a
realistic pre-#16081 board fixture (tasks + task_events both legacy
shape) — origin/main reproduces ``no such column: session_id``; this
branch migrates cleanly and creates all four indexes.
The SIGTERM/SIGHUP handler raised KeyboardInterrupt() at the end of its
agent-interrupt + grace-window sequence. Python delivers signals between
bytecodes on the main thread, so when the signal hit mid-event-loop
(typically inside prompt_toolkit's '_poll_output_size' coroutine's
'await asyncio.sleep()'), the KeyboardInterrupt unwound INTO that
coroutine. prompt_toolkit's Task captured it as a BaseException;
prompt_toolkit's '_handle_exception' then printed 'Unhandled exception
in event loop' + the full asyncio traceback and parked the terminal on
'Press ENTER to continue...' before exiting.
Same root cause as #13710, different surface: there the failure was an
EIO cascade after a logging-cache KeyError escaped the handler; here
it's the KBI raise itself landing inside an asyncio Task. The fix is
the same shape — let the event loop unwind on its own terms.
Now: schedule 'app.exit()' via 'loop.call_soon_threadsafe()'. The
prompt_toolkit Application returns normally from 'app.run()' and the
existing '(EOFError, KeyboardInterrupt, BrokenPipeError)' handler in
the input loop catches everything else. Fallback to 'raise
KeyboardInterrupt()' preserved for contexts where prompt_toolkit isn't
the active app (e.g. -q one-shot mode).
The agent interrupt + 1.5 s grace window run unchanged before the new
exit path, so subprocess-group cleanup ('os.killpg' on Linux) still
gets its window.
Tested live: external SIGTERM to the CLI (with 'kill <pid>') now exits
cleanly with no traceback dump and no ENTER pause.
Follow-up to #28455. The respawn guard's blocker_auth rule (last error
matched a quota/auth/429 pattern) was auto-blocking the task on first
occurrence. That's too aggressive: transient rate limits typically
clear in seconds to minutes, but the auto-block puts the task in
'blocked' status which requires manual unblock.
Now treats blocker_auth the same as recent_success and active_pr:
defer the spawn this tick, leave the task in 'ready', let the next
tick try again. If the auth error genuinely persists, the existing
consecutive_failures counter trips the auto-block circuit breaker
after failure_limit failures via the normal path — so a persistent
401/403/quota-exhausted still ends up blocked, just not on first hit.
Also documents the respawn_guarded event in kanban.md's events table
with the three guard reasons.
Updated test_dispatch_respawn_guard_auto_blocks_auth_error → renamed
to test_dispatch_respawn_guard_defers_auth_error_without_auto_block;
asserts task stays in 'ready' and the guard reason is recorded.
Five small fixes against issues filed during the post-merge salvage audit:
* #28670: `_GATEWAY_PROVIDER_ERROR_RE` false-positives on legitimate prose.
Replace the regex with an anchored `_GATEWAY_PROVIDER_ERROR_SHAPE_RE` and
add a length-cap heuristic to `_looks_like_gateway_provider_error`:
short envelope at the start of the message → real provider error; long
prose containing 'HTTP 404' → assistant answer, leave alone.
* #28672: drop the pointless 1s asyncio.sleep on Telegram thread-not-found
retries. The same-thread retry is preserved (catches Telegram's
occasional transient flake exercised by
test_send_retries_transient_thread_not_found_before_fallback) but with
no artificial delay.
* #28674: broaden `_should_retry_without_dm_topic_reply_anchor` to also
fire when Bot API rejects `direct_messages_topic_id` for synthetic /
resumed sends that have no reply anchor. Avoids dropping post-resume
background notifications if the topic id goes stale.
* #28676: delete the dead image-document branch superseded by bd0c54d17
(which returns early on the same extension set).
* #28678: extend chat-scoped allowlist (`TELEGRAM_GROUP_ALLOWED_CHATS`)
to also cover `chat_type == 'channel'`, so operators can authorize
channel posts by chat id without falling back to per-user allowlists.
Tests:
- scripts/run_tests.sh tests/gateway/test_telegram_thread_fallback.py -q → 41/41
- scripts/run_tests.sh tests/cron/test_scheduler.py -q → 127/127
- broader test set: same 3 pre-existing test-pollution failures reproduce
on plain main.