Counted with an open()/read_text audit hook on a real 'hermes --version'
run: config.yaml was parsed 3x before load_config() even ran — once by
env_loader._load_secrets_config, once by main.py's early redact/ipv4
bridge (bespoke yaml.load), once by hermes_logging._read_logging_config.
Each raw parse is 1-3 ms with libyaml plus an open/stat — pure
duplication since all three want the same raw dict.
All three now route through read_raw_config()'s existing (mtime_ns,
size)-keyed shared cache:
- env_loader._load_secrets_config: uses the shared reader when reading
the process HERMES_HOME (the cache key's home); other homes (profile
seeding) keep the isolated direct parse. Parse-error isolation is
preserved — the shared reader also swallows errors and returns {}.
- main.py early bridge: drops the bespoke yaml.load for read_raw_config
(managed-scope overlay unchanged).
- hermes_logging._read_logging_config: prefers the shared reader,
falls back to the direct fast_safe_load parse when hermes_cli.config
isn't importable.
Measured: config.yaml opens per 'hermes --version' 3 -> 1.
348 targeted tests green (env_loader + secret sources + applied-homes +
bitwarden + hermes_logging + config).
Four fixes on the updater path:
1. uv self update freshness gate + timeout (managed_uv.py): the network
self-update ran on EVERY hermes update — including the 'Already up to
date!' fast path — with NO timeout (unbounded hang risk offline). Now
skipped when it succeeded within 7 days (stamp file under
HERMES_HOME/cache), capped at 60s, force= override available. The
CVE-driven vulnerable-runtime repair probe is NEVER gated — it still
runs on every invocation.
2. Drop the second network fetch from the pull step (main.py): the update
flow fetched origin/<branch>, counted commits, then ran
'git pull --ff-only origin <branch>' — a SECOND fetch of the same ref
(~0.5-1.5s). Now merges the already-fetched tracking ref via
'git merge --ff-only origin/<branch>'; the diverged-history reset
fallback is unchanged.
3. Probe the upstream remote locally before fetching it (_cmd_update_check):
non-fork installs have no 'upstream' remote, and --check burned a
failed network attempt (~0.3-1s) on every run before falling back to
origin. 'git remote get-url upstream' (~1ms local) now gates the fetch.
4. Desktop rebuild check reads the content-hash stamp in-process before
spawning 'hermes desktop --build-only' (a full CLI re-import, ~1-3s)
just to learn nothing changed. Stamp errors fall through to the
subprocess path unchanged.
Savings on a no-op 'hermes update': ~2-6s (uv self-update 0.5-3s +
second fetch 0.5-1.5s + desktop spawn 1-3s when applicable).
187 targeted updater tests green incl. 5 new stamp-gate tests.
The import-agent subcommand (24c3c27ba8) shipped with its parser builder
and handler logic but build_import_agent_parser() was never called from
main.py, making the documented and unit-tested command uninvocable.
Register it alongside the other subcommand builders.
Hardened-runtime restrictions are enforced even for ad-hoc signatures,
so signing with --options runtime without the allow-jit entitlements
would leave Electron/V8 crashing on launch — strictly worse than the
legacy plain ad-hoc sign. Raise instead, so the fixup falls back to the
legacy path and the bundle always stays launchable.
Local/self-updated macOS builds were finished with a plain
'codesign --force --deep --sign -', leaving a cdhash-only Designated
Requirement and stripping electron-builder's entitlements. Every rebuild
changes the cdhash, so TCC treats the new bundle as different code and
forgets Full Disk Access, Desktop/Downloads/Documents, Accessibility,
Automation, and microphone grants — users re-approve everything after
every update.
Rework the relaunch fixup to sign inside-out (standalone Mach-O
binaries, nested frameworks/helpers, then the main bundle), preserving
the repo's entitlement plists, and pin an identifier-based Designated
Requirement when signing ad-hoc so TCC has a stable identity to persist.
Opt-in desktop.macos_signing_identity names a persistent keychain cert
(self-signed Code Signing cert works — no Apple Developer account) for a
certificate-anchored DR, the strongest form. An intact Developer ID
signature is detected and never clobbered, callers can pass the
publisher-signing decision explicitly so a later dotenv load can't flip
it, and the legacy deep ad-hoc sign remains the last-resort fallback.
Co-authored-by: lewis4x4 <lewis4x4@users.noreply.github.com>
Co-authored-by: natebransc <natebransc@users.noreply.github.com>
Co-authored-by: caseyanthony <caseyanthony@users.noreply.github.com>
Co-authored-by: gvago <gvago@users.noreply.github.com>
Co-authored-by: twe-cloud <twe-cloud@users.noreply.github.com>
Existing installs predate the install.sh hermes-acp launcher, and hermes
update never re-runs setup_path, so ACP hosts (Zed, JetBrains, Buzz)
still resolve Hermes as unavailable until a reinstall. _ensure_acp_launcher()
writes the launcher next to an existing hermes command in ~/.local/bin or
/usr/local/bin during hermes update — delegating to the sibling launcher so
it is correct for every install layout. Never follows symlinks (#21454),
skips unwritable dirs, no-op on Windows (venv Scripts is already on PATH).
Docs: add a Buzz Desktop section to the ACP page (en + zh-Hans).
Closes#54678
`hermes photon ...` could fail with argparse `invalid choice: 'photon'`
even when the bundled Photon platform plugin is present. Photon registers
its top-level CLI command from the platform adapter module via
`ctx.register_cli_command(name="photon", ...)`, but bundled platform
plugins are cheap-registered as *deferred* entries to avoid importing every
gateway SDK during normal startup.
On the unknown-top-level-command slow path, `discover_plugins()` records the
deferred loader but never imports the matching platform module, so the CLI
registration side effect doesn't run and `photon` stays absent from
`_cli_commands` — argparse then rejects it.
Fix: after `discover_plugins()` on that slow path, resolve only the deferred
platform whose name matches the first positional token (via
`platform_registry.get(name)`) before reading `_cli_commands`. This imports
exactly the targeted platform, leaving normal startup cheap (a bare `hermes`
or flags-only invocation has no positional token and touches nothing). The
resolution is best-effort: registry/import failures are logged at debug and
never crash startup.
Added 3 tests in tests/hermes_cli/test_startup_plugin_gating.py: resolves the
matching platform, ignores empty/None command, and swallows registry errors.
Fails without the fix (symbol absent).
The stale-.pyc bug class (#6207, #60242, live WhatsApp report: gateway
ImportError 'cannot import name parse_model_flags_detailed' after /update)
has one shared shape: the checkout's .py files change while __pycache__
retains bytecode from the previous revision, and a later process trusts
the stale .pyc.
Update-time clears can never fully close this class: 'hermes update'
always executes the PRE-pull updater code, so hardening added to it takes
effect one update late — and a manual 'git pull' never runs the updater
at all.
Class fix:
- Launch-time guard in main(): compare the checkout fingerprint (cheap
file reads via _read_git_revision_fingerprint, no git subprocess)
against a .bytecode-fingerprint stamp; sweep __pycache__ once when they
diverge. Covers manual pulls, old updaters, ZIP restores — every entry
point (CLI, gateway service, desktop backend) passes through main().
- Record the stamp at all three update-time clear sites (git path pre-
install, git path post-install, ZIP path) so a normal update never
triggers a redundant launch sweep.
- Sibling site: 'hermes plugins update' + dashboard plugin update now
clear __pycache__ under the plugin dir after git pull (plugin trees
live outside the repo guard).
E2E validated: reproduced the stale-pyc shadowing (same-size same-mtime
source swap → old symbol wins in a fresh process), confirmed the launch
sweep restores the new symbol, no-ops when the checkout is unchanged,
and re-sweeps on the next pull. Sabotage-tested: reverting the sweep
fails 4 of the new tests.
Refresh update-sensitive modules before lazy backend refresh so an in-place git update does not keep using pre-pull module objects when newly pulled code imports fresh helpers.
Constraint: Issue #60242 reports post-update lazy backend refresh importing stale hermes_constants after a large Windows update.
Rejected: Only clearing __pycache__ earlier | the update process can still hold old modules in sys.modules.
Confidence: high
Scope-risk: narrow
Directive: Keep update-time lazy refresh guarded against in-process code skew after git pull.
Tested: ./.venv/bin/python -m pytest tests/hermes_cli/test_update_autostash.py::test_cmd_update_reloads_runtime_modules_before_lazy_refresh tests/hermes_cli/test_update_autostash.py::test_reload_updated_runtime_modules_restores_new_hermes_constants_symbol -q
Tested: ./.venv/bin/python -m pytest tests/hermes_cli/test_update_autostash.py -q
Tested: ./.venv/bin/python -m ruff check hermes_cli/main.py tests/hermes_cli/test_update_autostash.py
Tested: ./.venv/bin/python -m py_compile hermes_cli/main.py tests/hermes_cli/test_update_autostash.py
Tested: git diff --check
Not-tested: Windows v0.14.0 to v0.18.0 end-to-end update.
Reshapes the salvaged fix so it recovers the `tsc: not found` build without
mis-diagnosing healthy trees.
The npm config forcing is dropped. It rested on the premise that an inherited
`npm_config_omit=dev` can beat the `--include=dev` CLI flag; npm resolves
command-line flags above environment config and filters `omit` by `include`,
so the flag already wins. Verified on npm 10 and 11.5.1: with
`npm_config_omit=dev` set and `--include=dev` passed, devDependencies install.
`npm_config_production` is worse than redundant — npm 9 removed it, so setting
it prints `npm warn config production Use --omit=dev instead.` on every
install.
The readiness probe now reads every root `npm run build` searches, not just
the workspace root. npm links a package's bin shims under the package itself
when it owns its lockfile (#42973), so a root-only check called a working tree
broken: it forced a redundant install, skipped the build entirely, and made
`hermes web` exit 1 on a layout that builds fine today.
The pre-build probe is gone with it. A build that works is never second-guessed
and no filesystem introspection gates it; recovery is driven by the failure
instead. When the build cannot resolve tsc or vite, we reinstall (visibly) and
retry before the generic delayed retry, which otherwise just reruns the same
command and leaves the stale dist in place forever. The lockfile-hash skip
still invalidates on an incomplete toolchain so the next update repairs itself.
Also drops the branches that changed behavior based on whether a test mock was
installed, and replaces the mock-call-count tests with real temp trees covering
both hoisting layouts, the Windows shim extensions, and each shell's wording of
an unresolvable binary.
Co-authored-by: Gerardo Camorlinga Jr. <gercamjr.dev@gmail.com>
hermes update could report a successful workspace npm ci while
devDependencies were omitted (production/omit-dev env leakage), leaving
tsc/vite unlinked. The subsequent web UI build then failed with
`tsc: not found` and fell back to a stale dashboard dist.
Force npm_config_include=dev on install, verify toolchain shims after
workspace install (and refuse to hash-cache a half tree), repair/reinstall
when node_modules exists without tsc/vite, prepend workspace .bin dirs to
PATH, and retry the build after a tsc/vite-not-found failure.
`hermes -c`/`--resume` (continue last session) resolved the globally
most-recently-used session, then cd'd into *its* recorded cwd. So running
`hermes -c` from repo A could land you in repo B's session — the session
you last touched anywhere, not the last one *here*.
Now `_resolve_last_session` scopes to the current workspace first: the git
repo root when CWD is inside a repo (so all sessions across its
subdirs/worktrees group together), else the CWD itself — matching the
`workspace_key` identity `hermes sessions list --workspace` already groups
on. It falls back to the unscoped global MRU when no session matches the
current workspace, preserving the old behaviour for fresh directories.
Adds `workspace_key` param to `SessionDB.search_sessions` and a
`_workspace_key_clause` SQL helper that mirrors `workspace_key()`: a row
matches when its `git_repo_root` equals the key, or (legacy rows without
git metadata) when its `cwd` is at or under it.
Surgical reapply of #53505 by @LeonSGP43 onto current main (stale-base
cherry-pick conflicted in main.py):
- _refresh_active_memory_provider_dependencies() in hermes_cli/main.py,
wired into BOTH the git-pull update path (after lazy refresh) and the
ZIP update path — the provider's plugin.yaml bridge packages are not
in extras or LAZY_DEPS, so the core reinstall could strip/downgrade
them and the update flow never healed them (#53272 mem0ai).
- _install_dependencies(force=True) in memory_setup.py: hand every
declared spec to the resolver on update so missing AND version-drifted
packages are restored (no-op when satisfied).
- Skip guards: no provider / builtin store / memory.enabled=false.
Widened beyond the original PR for the #70636 half:
- _provider_pip_dependencies(): mode-aware expansion — Hindsight in
local/local_embedded mode needs hindsight-all (daemon + embedder), not
just the declared hindsight-client; setup installs it but plugin.yaml
can't express it, so update-time healing previously missed
hindsight-embed and the daemon stayed broken.
- Spec-aware import probing: version ranges in pip_dependencies
(mem0ai>=2.0.10,<3) no longer break the pip-name -> import-name
mapping.
Fixes#53272. Fixes the hindsight-embed half of #70636.
Two halves close the 'legacy pythonw gateways survive updates forever' gap:
1. hermes update now regenerates the installed Scheduled Task / Startup
launcher scripts (gateway.cmd + gateway.vbs) during the gateway resume
phase. They are persistence artifacts written once at install time;
updates never touched them, so pre-aa2ae36c3f installs kept launching
the gateway through pythonw.exe forever — every descendant spawn
flashed a conhost (#54220/#56747) and, since #70344, the console-less
gateway died at startup with RuntimeError: sys.stderr is None (#71671).
The task /TR points at a stable script path, so rewriting the files
retargets it with no schtasks call and no UAC. No-op for modern
installs; best-effort so a failed refresh never fails the update.
2. _resolve_detached_python() normalizes a legacy pythonw.exe interpreter
to its sibling console python.exe when it exists, so the update
pause/resume argv-replay path (and any other caller handed a legacy
command line) respawns on the current design instead of faithfully
resurrecting the old one. Keeps pythonw when no sibling exists — a
failed respawn is worse than a console-less gateway.
- hermes dashboard --status now verifies each matched PID is alive AND
bound to a listening socket before reporting it, so stale PIDs and the
desktop app's IPC-only 'serve --port 0' backends no longer masquerade
as running dashboards (#58578).
- The git and Windows ZIP update paths share one
_finish_dashboard_update_cleanup(), so the ZIP fallback gets the same
stop/restart reporting.
- _kill_stale_dashboard_processes returns a structured
{matched, killed, failed, unrecovered} result; the explicit was-stopped
notice fires only for processes that could NOT be auto-restarted,
meshing with the auto-respawn from #72192.
'Refreshing cua-driver (Computer Use)...' could hang for minutes on
Windows: when the driver's native check-update verb returned an
indeterminate result (old driver without the verb, offline, GitHub
rate-limited, or the probe timing out), install_cua_driver(upgrade=True)
fell through to the full upstream installer — a silent, output-captured
run with a 660s ceiling, plus install.ps1's 600s concurrency-lock wait
on Windows on top. Every 'hermes update' paid that cost.
Two changes:
- install_cua_driver() grows require_confirmed_update: with it set, an
indeterminate check keeps the installed version and returns fast,
printing the force path (hermes computer-use install --upgrade).
'hermes update' passes it; the explicit --upgrade CLI keeps the old
fall-through so a force refresh still works when the check can't
answer.
- cua_driver_update_check() default timeout is now 25s on Windows
(8s unchanged on POSIX): first-spawn of the exe under Defender /
SmartScreen routinely exceeds 8s, and a false timeout is exactly the
indeterminate result that used to trigger the multi-minute reinstall.
Capture each manually-started dashboard/serve process's argv before the
stale-process kill (/proc/<pid>/cmdline on Linux, ps -o command= on macOS),
then respawn it detached after the update — headless (--no-open) with output
to logs/dashboard-restart.log under the active profile's HERMES_HOME.
Supervised PIDs keep their systemd-unit restart; --stop stays a plain stop.
Salvaged from PR #41508 with scope fixes: serve matching preserved, profile-
aware log path, restart only on the update path (restart_managed=True).
Previews correct themselves on read, but a title already written to the DB
stays wrong. This regenerates those titles from what the user actually typed,
dry-run by default.
Two guards, both hit on a real store: a candidate that isn't title-shaped is
rejected rather than replacing a serviceable title with command output, and a
unique-title collision dedupes through the lineage the way the live auto-titler
does instead of leaving the leaked title in place.
Adds build_aux_picker_rows() + format_aux_picker_entries() to
hermes_cli/inventory.py and converts both auxiliary-task pickers to them,
so custom providers, exclusions, and picker visibility can no longer be
dropped one call site at a time.
The two salvaged commits each fixed one kwarg at these same two sites
(user/custom providers, then for_picker). Both were per-site patches, so
the next aux picker would have reintroduced the gap. The seam makes the
correct behaviour the default a new caller cannot forget:
- user `providers:` and saved `custom_providers:` entries
- `model_catalog.excluded_providers`, matching /model
- exhausted-credential-pool providers stay visible
- only the active custom endpoint is probed, so an offline saved local
server can't hang the picker
- the virtual `moa` row is excluded (auxiliary_client unwraps it to the
aggregator anyway, so offering it is a silently-rewritten choice)
The vision picker also gains the current-selection marker it never had.
Also threads for_picker through build_models_payload, which had no way to
express it despite list_authenticated_providers supporting it.
credential pool has entries but is entirely rate-limited (exhausted): rate
limits are per-model for many providers (e.g. Google Gemini), so an exhausted
key for model-A may still serve model-B, and the user should stay able to
select it. That fix wired `for_picker=True` into `list_picker_providers`.
The two sibling interactive pickers reached by `hermes model` still called
`list_authenticated_providers()` WITHOUT `for_picker=True`, so they kept
hiding exhausted-pool providers:
- `_aux_select_for_task` (hermes_cli/main.py) — the auxiliary-task
provider/model picker.
- `_configure_vision_provider_model` (hermes_cli/tools_config.py) — the vision
provider/model picker.
Both persist a per-task config the user runs *later* (once the momentary
cooldown clears), so hiding an exhausted-pool provider here is exactly the
Tests: two regressions assert each picker requests `for_picker=True` (they
omitted it before the fix); the existing picker/exhausted-pool suite still
passes (6 total).
The residual Windows-on-ARM integrity-gate fix added GetNativeSystemInfo as a
second "OS-native" probe behind IsWow64Process2. Microsoft documents the
opposite behavior: "the API GetNativeSystemInfo also returns emulated
processor details when run from an app under emulation."
On the exact hosts the probe was added to rescue -- an x64 hermes-setup.exe
emulated on an ARM64 Surface -- it therefore reports AMD64, making it a
duplicate of the PROCESSOR_ARCHITECTURE rung already below it rather than a
fallback for it. Its test only passed because the kernel32 fake was hand-fed
ARM64, asserting behavior real Windows does not exhibit.
Replace it with GetMachineTypeAttributes, which answers the question the gate
actually asks -- "can this host load a PE of machine X?" -- instead of
inferring it from an architecture name. It is also the only documented API
that reports AMD64-on-ARM64 emulation support. The gate prefers it and falls
back to the existing name-based mapping on pre-Windows-11 hosts.
The load-bearing half of the previous fix (typing GetCurrentProcess as HANDLE
so IsWow64Process2 stops failing ERROR_INVALID_HANDLE) is unchanged.
Co-authored-by: xxxigm <xxxigm@users.noreply.github.com>
Co-authored-by: Teknium <teknium1@users.noreply.github.com>
A failed `hermes sessions repair` printed "keep state.db and the backup"
and stopped, so a user whose sessions had vanished had no way to discover
that a non-destructive recovery path exists — the reported dead end.
The failure branch now names the next command, read-only step first, seeded
with the backup path it just preserved. Covered by a CLI-surface regression
test that drives the real subprocess against an unrepairable database.
* fix(update): bind IsWow64Process2 HANDLE and fall back to GetNativeSystemInfo
The #71218 OS-native probe still rejected correct ARM64 Desktop rebuilds on
Windows-on-ARM when ctypes truncated GetCurrentProcess()'s pseudo-handle and
IsWow64Process2 failed with ERROR_INVALID_HANDLE, falling through to the
lying PROCESSOR_ARCHITECTURE=AMD64 value. Type the HANDLE correctly and use
GetNativeSystemInfo before the env-var fallback.
* test(update): cover WoA IsWow64Process2 handle failure and system-info fallback
Pin the residual #71218 shape where IsWow64Process2 returns FALSE, the env
arch lies as AMD64, and GetNativeSystemInfo must still report ARM64 so the
integrity gate accepts a correctly-built ARM64 Hermes.exe.
`sync_skills()` keys the bundled manifest by frontmatter name but computes
the destination from the bundled path. When upstream renames or
recategorizes a skill, the manifest key still matches while the new dest
does not exist yet, so the loop fell into its "in manifest but not on
disk" branch and misread the skill as user-deleted: the user's copy was
stranded at the old path forever and never received another update.
Three skills hit this in the July 2026 reorg (computer-use,
evaluating-llms-harness, serving-llms-vllm) — silently frozen at their
pre-rename content on every machine that ran `hermes update`.
Recovery only moves a stale copy when it is byte-identical to the origin
hash recorded the last time sync wrote it, which proves the directory is
ours rather than the user's work. User-modified copies are kept in place
with a warning, hub-installed paths are never touched, and a genuine
deletion (no copy anywhere on disk) is still respected.
- tools/skills_sync.py: add _recover_renamed_skill() plus the
_index_active_skills() / _read_hub_install_paths() indexes; call it
before classification and report moves via a new `relocated` key.
- hermes_cli/main.py: surface relocations in both `hermes update` skill
sync reporting sites.
- tests: 4 cases covering relocate, user-modified preservation,
hub-installed exemption, and genuine-deletion respect.
The #71119 integrity gate rejected CORRECT ARM64 desktop rebuilds on
Windows-on-ARM (#69179 follow-up): platform.machine() reports the PROCESS
architecture, and the update chain runs an x64 hermes-setup.exe / x64
Python under ARM64 emulation, so the gate compared the ARM64 Hermes.exe
against a phantom 'AMD64 host' and aborted the update.
_windows_native_machine() now asks the OS via IsWow64Process2 (whose
nativeMachine is truthful from emulated processes), falling back to
PROCESSOR_ARCHITEW6432/PROCESSOR_ARCHITECTURE (pre-1511 Win10) and then
platform.machine(). All three consumers — the integrity gate, the
packaged-exe arch preference, and backup validation — go through it.
Sabotage-verified: the three new regression tests fail against the old
process-arch behavior and pass with the fix.
Builds on @ms-alan's label fix: the negative figure had a second cause
that relabelling alone leaves in place.
`hermes sessions optimize-storage` reported "reclaimed -3820.1 MB" on a
database that had in fact shrunk 60% (25069 MB -> 9975 MB). Both figures
came from os.path.getsize(). In WAL mode a VACUUM's rewrite lands in the
-wal file, and the checkpoint that folds it back is REFUSED
(SQLITE_BUSY) while any other connection holds a read-mark — e.g. the
live gateway. So the main file still carried its pre-VACUUM size AND kept
growing while the command ran, making the after-figure larger than the
before-figure. The TRUNCATE checkpoint in close() lands after the caller
has already measured and printed.
- hermes_state.py: add SessionDB.logical_size_bytes() — page_count *
page_size, the size the file settles at once the WAL is folded back.
Correct immediately, readers or not; returns None when the connection
is gone so callers fall back to stat().
- hermes_state.py: best-effort TRUNCATE checkpoint after the optimize
VACUUM so the file settles promptly when nothing else holds the DB.
Documented as insufficient alone (busy under a live gateway) so the
stat() approach is not reintroduced.
- hermes_cli/main.py: both `optimize-storage` and `optimize` report via
logical_size_bytes(); fixing only the reported command would have left
the same bug class next door.
- hermes_cli/main.py: lift the contributor's inline label to a
module-level _size_delta_label() shared by both sites, so the "grew by"
wording is consistent and unit-testable without reading source.
The two halves are complementary: page accounting removes the phantom
negative, and "grew by" still covers a genuine increase when concurrent
session writes outweigh what the rebuild freed.
Verified: sabotaging logical_size_bytes() back to stat() fails the new
test with a 22 MB overstatement, reproducing the report. The test asserts
its own precondition (stat() must actually be lagging) so it cannot
silently stop exercising the bug.
Tests: 464 passed (test_hermes_state.py + the new label tests).
Co-authored-by: chenbin <h-chenbin@voyah.com.cn>
Closes#70146
When hermes sessions optimize-storage runs and the database grows
(rather than shrinks), the old code always printed '(reclaimed X MB)'
with a negative value. Fix by using a conditional label:
- positive delta → 'reclaimed X MB'
- negative delta → 'grew by X MB'
The post-update state.db integrity guard called verify_sqlite_integrity()
with max_bytes=0, which disables the size ceiling and forces a full
PRAGMA integrity_check. That pragma walks every b-tree page in the file,
so its cost scales with database size — measured on a real 30 GB state.db:
143.5s with a cold page cache (worse under an update's memory pressure),
with zero output on screen. The update looks hung right after
"✓ Code updated!" and a CPU sits pegged.
Multi-GB session databases are normal for heavy users, so a
size-unbounded check is never an acceptable default on the update path.
- verify_sqlite_integrity(): max_bytes now defaults to
DEFAULT_INTEGRITY_CHECK_MAX_BYTES (2 GiB) instead of 0. max_bytes=0
remains the explicit opt-in for a full scan.
- The oversized path no longer degrades to a header-only check: it adds a
constant-time structural probe (read-only open + schema_version +
sqlite_master read) so the malformed-schema class is still caught, not
just the #68474 zeroed-file signature.
- Drop the explicit max_bytes=0 at the post-update guard and in
copy_db_and_verify() so both inherit the bounded default.
Measured on the reporter's real 30 GB state.db: 143.5s cold → 0.001s,
still valid=True. Corruption detection verified at multi-GB scale for
both classes (zeroed header, malformed schema) — both still fail closed.
Tests: default-is-bounded invariant, oversized probe catches malformed
schema, max_bytes=0 still forces the full check.
Follow-up cleanup for PR #71123:
- Extract getattr(args, 'lineage', 'single') == 'logical' to a local
(appeared 3x in the export block)
- Document that the double _collect_delegate_child_ids traversal in
delete_session is an intentional TOCTOU guard inside the write txn
hermes model's custom-endpoint flow is the other write path that produced a
plaintext key, on both the model block and the custom_providers entry. Route
it through the same .env indirection as the Desktop panel, and swap an
existing entry's inline key for the reference when the URL is re-saved.
Co-authored-by: liuhao1024 <sunsky.lau@gmail.com>
The desktop self-update chain (Desktop -> hermes-setup --update ->
hermes update -> hermes desktop --build-only -> relaunch) rebuilds
Hermes.exe on the user's machine and declared success on bare file
EXISTENCE. A truncated PE (corrupt cached Electron zip / interrupted
extraction or rcedit rewrite / full disk) or a wrong-architecture
unpacked tree therefore shipped as the 'updated' app, which Windows
refuses to load with 'This app can't run on your computer'
(此应用无法在你的电脑上运行) — and the previous working build had
already been wiped by before-pack.mjs, leaving nothing to fall back to.
Fix, in three parts:
- hermes_cli/main.py: post-build integrity gate on Windows
(_ensure_desktop_exe_launchable). Parses the PE header of the freshly
built Hermes.exe — MZ/PE magic, section-table completeness vs file
size (catches truncation), and COFF machine vs the host arch (catches
arm64/x64 mixups). On failure it purges the (likely corrupt) cached
Electron zip, invalidates the content-hash build stamp so the
updater's retry-once genuinely re-downloads and rebuilds, restores
the previous build from the .bak tree when one exists (keeping the
corrupt tree as .corrupt for diagnostics), tells the user the update
was aborted and their old version kept, and exits nonzero.
_desktop_packaged_executable also now prefers a host-loadable PE over
pure newest-mtime when multiple win-*-unpacked trees coexist.
- apps/desktop/scripts/before-pack.mjs: on win32, the previous unpacked
tree is preserved as <appOutDir>.bak (only when it holds the product
exe — partial/corrupt trees still get the plain wipe) instead of
being destroyed, providing the rollback material for the gate above.
Non-Windows behavior is unchanged.
- Behavior-contract tests: tests/hermes_cli/test_desktop_exe_integrity.py
(23 tests — synthetic PE fixtures for truncation/non-PE/arch-mismatch,
rollback semantics, and the build-only exit contract) and 6 new vitest
cases in before-pack.test.mjs for the .bak preservation rules.
Progresses #69179
The cherry-pick auto-resolution + AST sweep applied plain utf-8 at three
.env reader sites where the salvaged PRs (#62617, #62123) deliberately
use utf-8-sig — a Notepad BOM must not hide/duplicate the first key.
Restore the contract (tests pin it).