/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.
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.
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.
#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.
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.
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.
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.
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.
_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).
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.
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.
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.
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).
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
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.
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).
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.
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.
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.
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.
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.
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.
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').
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.
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.
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.
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.
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().
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>
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)
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>
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.
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>