Commit Graph

50434 Commits

Author SHA1 Message Date
Teknium
889eddcd86 fix(gateway): /yolo survives a gateway restart
/yolo on a messaging platform only flipped tools.approval's in-memory
set, so any gateway restart (hermes update, crash, systemd restart,
Desktop reopen) silently put the chat back on manual approvals while
the user still believed YOLO was on.

The flag now lives on the chat's routing entry (SessionEntry.yolo,
persisted in gateway_routing like the /model override). /yolo writes it
before flipping the live flag, materializing the entry for a
first-message toggle; every turn re-arms the live flag from the entry it
already resolved; switch_session carries it across compression-tip and
binding repoints; the conversation-boundary security funnel (/new,
/resume, /branch, auto-reset) clears the persisted copy with the
in-memory one so a restart can never revive a bypass the user dropped.

Also corrects the security docs, which still claimed /yolo sets
HERMES_YOLO_MODE.
2026-10-07 16:15:35 -07:00
Teknium
3b288d8562 chore(plugin-catalog): bump hermes-monitoring-dashboard to 96d6881d
Pins austinpickett/hermes-monitoring-dashboard#9 (dashboard.json is never
lost to the wrong folder) and #10 (the dashboard opens only as a docked
side pane beside the chat; the route falls back to the in-place page on
desktops without host.openWorkspace). Version label stays 1.0.1: upstream
did not bump plugin.yaml and updates are offered by sha, not version.
2026-10-07 16:09:07 -07:00
Teknium
19cb1cbfed fix: disclose enabled plugins' registered skills in the system prompt's available_skills index 2026-10-07 16:01:10 -07:00
Teknium
cc75e8f402 chore(plugin-catalog): bump hermes-monitoring-dashboard to e4825262
Upstream PR #8 (skill text only): the compose-monitoring-dashboard skill now
claims generic "create a dashboard" requests and defines the short demo
commands (blank / add a widget / complete dashboard). No code, manifest,
dependency or egress change; plugin.yaml version stays 1.0.1, so the label is
unchanged. Disclosure still accurate.
2026-10-07 15:38:28 -07:00
Teknium
134e08ca6d fix(ci): route web_build_limits.py to the update e2e lanes
#133931 made source_build.py import hermes_cli/web_build_limits.py but did not add it to
_UPDATE_DEPENDENCIES, so tests/ci/test_update_ci_routing.py has been red on main and on
every PR whose merge ref includes it: a change to the module would skip the
e2e_upgrade and e2e_desktop_update lanes that run the build it shapes.
2026-10-07 15:26:14 -07:00
Teknium
c538ec5f40 test(agent): release the lease-test lock on a refusal signal, not a timer
Greptile P2 on #134577: time.sleep(0.5) could release the lock before the renewal ever hit it, passing without exercising a delayed renewal. Signal from the store's retry sleep (only reached after a real lock refusal) and assert the row was renewed.
2026-10-07 14:24:22 -07:00
Teknium
b8788c3cb1 fix(agent): bound lease lock tolerance by the row's committed expiry
Review of #134577 found two ways the lock tolerance could still outlive
the lease row, letting a successor reclaim it under a running turn:

- The local deadline was taken AFTER acquisition/renewal returned, while
  the store stamps expires_at BEFORE its write-lock wait, so a renewal
  that waited 20s gave the turn 20s more than the row. The deadline now
  starts from the admitted row's committed expiry
  (SessionDB.session_turn_lease_expires_at) and from a timestamp taken
  before each successful renewal; neither can exceed the row.
- A retry that started before expiry could block in the write-lock wait
  past it. Each renewal's write patience is now capped at the remaining
  authority minus a 2s margin (refresh_session_turn_lease patience_s),
  and the next-tick check budgets that margin too.

Test: real SessionDB + a second-connection write lock. Red on the PR head,
green here; the lock-clears control renews with no interrupt.
2026-10-07 14:24:22 -07:00
686f6c61
23cd990268 fix(agent): stop a locked lease refresh once its lifetime runs out
A SQLite lock is still a missed tick, but only while the next attempt
lands before the row expiry. Past that, acquisition can hand the session
to someone else while this turn is still active.
2026-10-07 14:24:22 -07:00
686f6c61
bae3a01012 fix(agent): do not interrupt a turn when lease refresh hits a SQLite lock
A locked state.db is contention, not a lost holder. Keep the refresher armed so the next tick retries instead of stopping a healthy turn.
2026-10-07 14:24:22 -07:00
Austin Pickett
9aaf2fda3c refactor(kanban): split board metadata/lifecycle into kanban_db_boards; fix ratchet findings
Move board-metadata and board-lifecycle functions (board_metadata_path,
read/write_board_metadata, _default_board_display_name, _dir_holds_board,
create_board, list_boards, remove_board) out of the kanban_db facade into
the topical sibling hermes_cli/kanban_db_boards.py, offsetting this PR's
facade growth past its main-pinned FILE_LINES cap (moved code keeps its
cap; re-exported from the facade tail so imports/monkeypatching are
unchanged).

Also drop the blind except Exception in the dashboard's _ws_board_live:
read_board_metadata never raises (missing/malformed board.json yields the
synthesized entry), so the archived-tombstone check needs no try/except.
2026-10-07 17:21:31 -04:00
Austin Pickett
1a85226e22 fix(kanban): dashboard rejects stale archived/deleted board slugs
_resolve_board 404s archived tombstones, and the /events WebSocket closes
stale-tab streams (1008) at the handshake instead of handing the dead slug
to connect(), which resurrected an empty board (#43243).
2026-10-07 17:21:31 -04:00
Austin Pickett
cae57adfa0 fix(kanban): connect()/init_db() refuse to recreate dead boards
Refuse to create a fresh DB for a slug that is archived (tombstone
board.json) or does not exist (no board.json) — read/watch paths (dashboard
tabs, gateway notifiers, event streams) handed a stale slug used to mkdir an
empty DB-only stub that reappeared as an active board (#43243). Explicit
creation goes through create_board, which writes metadata first; the
current-board context resolving to an archived dir is refused too.
2026-10-07 17:21:31 -04:00
Austin Pickett
63e5656409 fix(kanban): board identity requires board.json; archive leaves a tombstone
A directory holding only kanban.db (no board.json) is a stub, never a
board: _dir_holds_board/list_boards/board_exists now require the metadata
file, so historical DB-only ghosts stop surfacing as empty active boards
(#43243).

remove_board(archive=True) leaves an archived tombstone board.json at the
original slug so stale readers can distinguish an archived slug from a
never-created one; create_board clears the tombstone (explicit creation
wins), get_current_board falls through archived slugs, and boards switch/rm
explain the tombstone in their output.
2026-10-07 17:21:31 -04:00
Austin Pickett
0b70608a58 test(gateway): venv import setup must not leak PYTHONPATH into global environ (#57467) 2026-10-07 17:19:01 -04:00
liuhao1024
6dc79b2ad8 fix(gateway): stop _ensure_windows_gateway_venv_imports leaking PYTHONPATH into global environment
Remove the global os.environ["PYTHONPATH"] assignment from
_ensure_windows_gateway_venv_imports(). The sys.path manipulation
is sufficient for the current process, and all Hermes child-process
spawners (code_execution_tool, gateway_windows, watcher) already
build their own PYTHONPATH explicitly in subprocess-specific env dicts.

Setting PYTHONPATH globally poisons every non-Hermes Python process
launched from a Desktop session (Unsloth, ComfyUI, etc.), causing
version conflicts and GPU initialization failures.
2026-10-07 17:19:01 -04:00
Austin Pickett
6869d009c9 fix(desktop): the link-title window cannot become visible
On Windows Desktop the hidden tier-2 link-title BrowserWindow has still
painted a blank top-level frame over the chat during browser-tool turns
(#64867). Encode the invisibility contract in the launch options —
off-screen bounds, skipTaskbar, focusable: false — and belt-and-suspenders
it with setOpacity(0) + hide() after creation, best-effort like the audio
mute.

Salvaged from #66523 (thanks @stantheman0128).
2026-10-07 17:13:33 -04:00
Austin Pickett
98900360e7 fix(browser): pin local Chromium headless on Windows
Local mode is documented as zero-cost headless Chromium, but nothing pins
that contract: any environment or config that resolves agent-browser's
--headed setting to a window turns a browser-tool turn into a visible
blank top-level frame over the Windows Desktop chat (#64867).

Extract windows_headless_browser_options(options, is_windows) — pure so
the invariant is testable on any host — and wire it into both local
Chromium creation paths: _spawn_and_collect (main session dispatch) and
_run_chrome_fallback_command (temp fallback session). It pins
AGENT_BROWSER_HEADED=false only on Windows, only for local launches, and
never overrides an explicit AGENT_BROWSER_HEADED (the opt-out stays the
user's). Headed mode (browser.headed / AGENT_BROWSER_HEADED) keeps its
intentional --headed argv.

Fixes #64867
2026-10-07 17:13:33 -04:00
Austin Pickett
cc11102273 docs(web): note the dashboard build's resource caps (#63338) 2026-10-07 17:13:11 -04:00
Austin Pickett
af9c7137c8 fix(web): bound the dashboard build's CPU and heap (#63338)
The dashboard build (Vite 8 + Rolldown + Tailwind v4) saturates every CPU
on small hosts (200%+ top readings, OOMs and frozen SSH sessions on 2–4
vCPU VPS boxes). Rolldown exposes no JS-level worker knob, but its native
binding parallelizes with rayon, which honours RAYON_NUM_THREADS.

Cap both sides of the build, everywhere it runs:

- scripts/build/web.mjs (the npm run build / packaging entry): limit the
  native bundler's rayon pool to half the available cores before vite is
  imported, and re-exec once with a --max-old-space-size cap (sized from
  the cgroup memory limit, 75%, floored at 1 GB, ceiling 4 GB) unless
  the user already set one.
- hermes_cli/web_build_limits.py + build_source_web(): the same caps for
  the install/update/dashboard-triggered source build path.

User env always wins (existing NODE_OPTIONS heap flag or
RAYON_NUM_THREADS is preserved); HERMES_WEB_BUILD_MAX_OLD_SPACE_SIZE /
HERMES_WEB_BUILD_THREADS tailor the caps, and HERMES_WEB_BUILD_LIGHT=1
tightens them (1 thread, 1 GB heap) for hosts that cannot spare full CPU
during the build — the light-build idea from the closed PR #78131.
2026-10-07 17:13:11 -04:00
Austin Pickett
9d7a4ab718 fix(dashboard): hide AuthWidget on 401 via ApiError status
fetchJSON now throws ApiError with a humanized message, so the
widget's legacy `msg.startsWith("401:")` check never matched and a
401 from /api/auth/me rendered "auth status unavailable" instead of
nothing. Read the status off the error object.

Part of GH issue 66223 (loopback /api/auth/me).
2026-10-07 17:13:07 -04:00
Tranquil-Flow
0f56e56a24 fix(dashboard): repair loopback auth identity frontend integration (#66295) 2026-10-07 17:13:07 -04:00
Mark S.
3cabfad57e feat(models): add anthropic/claude-haiku-5.5 to OpenRouter and Nous Portal catalogs (#134759) 2026-10-07 17:12:40 -04:00
Austin Pickett
649d6c0391 test(desktop): fold the MEDIA pdf card case into the existing media suite 2026-10-07 17:05:12 -04:00
Austin Pickett
dbbc8d7947 test(desktop): pin the preview height clamp's viewport growth and ceiling 2026-10-07 17:05:12 -04:00
Austin Pickett
7b9ce36a21 fix(desktop): widen the inline image preview envelope so stacked image sets stay viewable 2026-10-07 17:05:12 -04:00
Austin Pickett
39a01d069d fix(model-picker): keep custom provider rows and the current model honest across switches
Three defects in one class, backend + renderer:

- _lap_bare_custom_row only built the bare `model.provider: custom` row
  while the session was ON it; switching away made it vanish and the
  unconfigured-canonical fallback substituted a 0-model placeholder
  (#59702). Recover the endpoint from the config.yaml `model:` block when
  the caller's current-provider slice points elsewhere; is_current is then
  true only when that slice points back at the URL, and the live probe
  stays gated on refresh / probe-current.

- The Desktop Settings model page kept the previous provider's model
  selected after a provider switch (withActive() painted it as still
  selectable, and Apply would pin the new provider to the old model),
  and a named custom_providers entry the user just switched to showed 0
  models because the normal non-refresh options load probes only the
  CURRENT custom provider (#59063). A user-driven switch now clears the
  model and fires one refresh-scoped options fetch for an empty
  configured endpoint.

- groupModels derived model families solely from the provider catalog;
  an OpenRouter current model the catalog omits left the picker with no
  active-model row (#57534). Prepend the current model when the catalog
  lacks it.

Tests: red on base for the bare-custom regression, the settings switch,
and the missing-current-model row; green with the fix.
2026-10-07 17:04:44 -04:00
Mark S.
04dc7f846c chore(plugin-catalog): pin claude-subscription-directsdk to 4bc79c7 (Haiku 5.5) (#134749)
Moves the pin from 31b591f to 4bc79c7, the merge of hermes-plugin-claude-subscription-directsdk#120: Haiku 5.5 pinned at 1M, `haiku` alias moved to it, and its reasoning-off calls leave out the thinking disable. The card image follows the pin; version 0.3.3 matches the plugin.yaml.
2026-10-07 16:55:30 -04:00
hermes-seaeye[bot]
3090ef7731 fmt(js): npm run fix on merge (#134737)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-10-07 20:26:07 +00:00
Austin Pickett
354724fb08 style(desktop): sort the projects-sibling imports per perfectionist 2026-10-07 16:08:56 -04:00
Austin Pickett
bf8cf34c50 refactor(desktop): move sidebar projects copy into per-locale siblings for #73091
The undo-hide toast strings pushed 8 locale catalog files past their
main-pinned FILE_LINES caps (they may only go down). Offset the growth
the AGENTS.md way: extract the existing sidebar.projects block from each
over-cap file (en/de/es/fr/ja/ru/zh/types) into a topical sibling
<lang>_projects.ts following the <lang>_boot / <lang>_model_menu
convention. No key changes; locales/_keys.desktop.json verified unchanged.
No behaviour change — pure code move.
2026-10-07 16:08:56 -04:00
Austin Pickett
ec60e9a6d8 test(desktop): store-level dismiss/restore round-trip for auto projects
Adds a layout-store test beside the existing menu test: dismiss hides
the row, restore brings it back, restore is idempotent. Also drops an
unused helper the salvaged test file left behind.
2026-10-07 16:08:56 -04:00
Austin Pickett
734c35bbc7 fix(desktop): translate the undo-hide toast strings (de/es/fr/ru) for #73091
The salvaged fix only shipped en/ja/zh; the other bundled locales fell
back to English. Adds hiddenFromSidebar/undoHide for de, es, fr, ru.
2026-10-07 16:08:56 -04:00
Brooklyn Nicholson
d7cf4d8dc7 chore(desktop): regenerate locales/_keys.desktop.json for undo-hide keys 2026-10-07 16:08:56 -04:00
PRATHAMESH75
3b2a6b6e1a fix(desktop): make auto-project Hide from sidebar reversible via an Undo toast (#73091)
Hiding an auto-derived project only appended its id to the persisted
$dismissedAutoProjectIds with no restore path, so an accidental Hide from
sidebar was permanent: the toast carried no Undo, ⌘Z was unwired, and there
was no other control to bring the row back.

Add restoreAutoProject(id) mirroring restoreWorktree, and fire a success
toast with an 8s Undo action from removeAuto. The id is captured before the
dismiss so Undo still restores after the row unmounts / scope exits.
2026-10-07 16:08:56 -04:00
kshitijk4poor
daa9593a03 test(state): drop the unpinned rollback claim from the corruption-class test
Gate finding (quality + 2ab r2, Low, mutation-verified): the test name and
module docstring claimed the rebuild arm is "rolled back", but nothing pins
it — the mocked execute raises before any transaction opens, and deleting
the rollback() line (which pre-exists on main) leaves the test green. Name
the test for what it actually asserts: caught and reported.
2026-10-08 01:31:24 +05:30
kshitijk4poor
324850626a fix(state): name the user-facing repair commands in the FTS corruption error
Gate finding (2c r2, Low): the rebuild_fts malformed-image ERROR pointed at
repair_state_db_schema, an internal function no user can run. Reuse the
wording doctor_state / hermes_state_user_copy already give for the same
fts_index failure: 'hermes sessions repair' (or 'hermes doctor --fix').
2026-10-08 01:31:24 +05:30
kshitijk4poor
995da31b27 fix(state): only a malformed-image error gets the FTS offline-repair hint
Fold finding 1: the new `except sqlite3.DatabaseError` arm in rebuild_fts()
also caught IntegrityError/ProgrammingError and logged them as corruption
needing repair_state_db_schema (probe-C05-1). Merge the OperationalError and
DatabaseError arms (catch + rollback unchanged) and choose the log with the
existing hermes_state_errors.is_malformed_db_error(): error + repair hint
when malformed, the original 'FTS rebuild failed' warning otherwise.
optimize_fts() logs no repair hint, so it is left as is.
2026-10-08 01:31:24 +05:30
kshitijk4poor
8e0c55de71 test(state): assert the exact surviving-index count
Fold finding 3: `>= 1` also passed when the injected corruption never
matched (no corruption -> 2, probe-C05-3), so the test could not tell
"one index failed, the rest ran" from "nothing failed". Assert
len(_present_fts_tables()) - 1, computed before the patch.
2026-10-08 01:31:24 +05:30
kshitijk4poor
583e995a91 refactor(state): one WHY line on the rebuild_fts corruption arm
Fold finding 4: the comment narrated history and claimed the error escaped
"un-rolled-back" — wrong on an autocommit connection (isolation_level=None,
probe-C05-2). Keep only why the arm exists.
2026-10-08 01:31:24 +05:30
kshitijk4poor
86e4a57f30 test(state): drop the toothless in_transaction assert
Fold finding 2: SessionDB's connection runs with isolation_level=None
(autocommit), so in_transaction is False after a failed INSERT even with no
rollback at all (probe-C05-2) — the assert could never go red. Removed.
2026-10-08 01:31:24 +05:30
kshitijk4poor
609181fd6e test(state): plain close() in the FTS corruption fixture teardown
Fold finding 5: the try/except around d.close() swallowed nothing real
(SessionDB.close() is idempotent and does not raise here) and would hide a
genuine teardown failure. Plain d.close().
2026-10-08 01:31:24 +05:30
kshitijk4poor
a0b14df1bc fix(state): catch corruption-class DatabaseError in optimize_fts() too
optimize_fts() runs the same per-index loop as rebuild_fts() and had the
same OperationalError-only arm. SQLITE_CORRUPT surfaces as
sqlite3.DatabaseError (a sibling, not a subclass), so one corrupt index
aborted the loop and skipped optimizing the healthy sibling indexes;
vacuum() then swallowed the escaped error and reported 0. Widen the arm
to DatabaseError (OperationalError is its subclass, so the existing
behaviour is unchanged) to close the whole class.

Extend the salvaged test with an optimize_fts parametrize row (red
without this change) and fix the 'is_caused' -> 'is_caught' typo in the
test name.

Refs #133375

Co-authored-by: liuhao1024 <sunsky.lau@gmail.com>
2026-10-08 01:31:24 +05:30
liuhao1024
e9a981bb6c fix(state): catch the corruption-class DatabaseError inside rebuild_fts()
SQLITE_CORRUPT ("database disk image is malformed") surfaces as
sqlite3.DatabaseError, a *sibling* of OperationalError, not a subclass.
The in-place FTS rebuild is the recovery path for a corrupt index, so a
failing 'rebuild' command raises exactly the class the except arm must
cover — but rebuild_fts() caught only OperationalError, so the error
escaped the per-index loop un-rolled-back (#133375 behavior 4). The
library's own read/write probes already layer the two classes
(hermes_state_repair.py, #66296/#100227); rebuild_fts() was the one
recovery entry point without the DatabaseError arm.

Add the sibling arm: roll the statement back, keep the remaining indexes
in the loop, and log at ERROR that the offline repair path
(repair_state_db_schema) is needed, so callers keep the documented
0-means-no-progress signal instead of an escaped exception.

Discriminative tests: on unfixed code both fail (DatabaseError escapes);
on fixed code the full-corruption case returns 0 + rolled-back
connection + ERROR breadcrumb, and one corrupt index does not stop the
remaining indexes from rebuilding.

(cherry picked from commit b6d3e92f4c491c54028ea931ec00e6e584488e8d)
2026-10-08 01:31:24 +05:30
Brooklyn Nicholson
865ba906c1 fix(auth): return the user-local external CLI as one native path on Windows
The user-local known dir is spelled %USERPROFILE%/.local/bin, so a Windows hit came back
with mixed separators in provider status and in the spawned ACP argv. Normalize the
resolved path.

Co-authored-by: Finn763 <165816600+finn763@users.noreply.github.com>
2026-10-07 14:19:47 -05:00
Brooklyn Nicholson
e894a7f82a refactor(auth): move external-process CLI lookup into auth_external_process to keep auth.py inside the size ratchet 2026-10-07 14:19:47 -05:00
Brooklyn Nicholson
152792362c test(auth): drive copilot-acp runtime resolution through a real PATH executable 2026-10-07 14:19:47 -05:00
Brooklyn Nicholson
6db85489b2 fix(auth): keep relative external CLI command paths resolving from the launch cwd 2026-10-07 14:19:47 -05:00
Brooklyn Nicholson
cea740420e chore(tests): drop the stale _external_process_spec bare_which allowlist row
The PR's locate_command rewrite removed the bare shutil.which the row
pinned; the staleness check (test_resolution_allowlist_has_no_stale_rows)
correctly flagged it. The row's removal is the guard working as intended.
2026-10-07 14:19:47 -05:00
KoNit-K
3126389d98 fix(auth): discover external CLIs in user-local bin
Use the shared command resolver for external-process providers under restricted backend PATHs.
2026-10-07 14:19:47 -05:00
Brooklyn Nicholson
dd6ab89cb8 fix(pricing): price Bedrock Claude Opus 5 and 5.5 from the snapshot
Direct Anthropic Opus 5 is priced on main, but the Bedrock lane still
missed anthropic.claude-opus-5 and anthropic.claude-opus-5-5, so Bedrock
sessions on either model (including the static Bedrock picker's
us.anthropic.claude-opus-5-5) fell through to models.dev or recorded
unknown / $0. Bedrock bills Claude at Anthropic's per-token rates, so
reuse the direct rows' rates.

Refs hermes-agent 100848

Co-authored-by: RelaxJonh <92573950+RelaxJonh@users.noreply.github.com>
2026-10-07 14:18:26 -05:00