Testers reported the macOS app icon reads oversized next to other apps:
the full-bleed 1024 squircle master put the shape ~149px past Apple's
icon grid on every side. macOS icns targets now render from an in-memory
mac master built on new squircle-mac-{light,dark} backgrounds — the same
white/#0d1117 squircle at 824x824 (r=185.4) centered in 1024 with 100px
margins, with the girl box scaled by 824/1024 to keep her relative size
inside the shape. All non-mac targets keep the full-bleed squircle.
A Windows bundle whose venv never processed pywin32.pth (see the launcher
fix) fails 'import pywintypes' with ModuleNotFoundError; portalocker 3.x
has no msvcrt fallback, so CLH retries lock() 20x and raises 'Cannot
acquire lock after 20 attempts' — which handleError suppressed entirely.
Result: zero file logging, silently, on every affected install.
* probe portalocker once at import (scratch lock/unlock) and fall back to
stdlib RotatingFileHandler when it fails; the fallback disables rollover
(multi-process appends make Windows renames fail with WinError 32, the
#44873 trap CLH exists to avoid) and setup_logging() warns once
* the suppressed CLH lock timeout now warns once through the logging
system instead of vanishing
The minted launchers wired venv site-packages onto sys.path with a raw
insert (win32 wrapper) / PYTHONPATH (posix), neither of which runs .pth
files. pywin32.pth is load-bearing on Windows: it puts win32\lib on
sys.path, which is what makes 'import pywintypes' resolve — without it
portalocker's Win32Locker dies and concurrent-log-handler silently drops
every file-log record on Windows bundles.
* launcher_wrapper.py: site.addsitedir() for the site entry (repo first,
site directly after, .pth dirs last)
* launchers.py posix: same via HERMES_SITE env in the -c bootstrap
* pm/environment.py: prune_site_pth() drops _virtualenv.pth and the
__editable__ pointer (build-machine path) that must never run in a
sealed payload
* python_env.py: run the prune after every environment build
Two pre-Python readers anchored their awk patterns on the exact leading
whitespace of the machine-written json they parse:
- setup-hermes.sh read artifact-mirror.json with /^ "origin"/ (exact
two spaces), so any other one-member-per-line layout lost the mirror
fallback silently: the mirror url resolved empty and fetch_pinned
reported only the primary url it failed on.
- scripts/install.sh read packages.python.version from pm/lock.json
with exact 4-space and 6-space anchors; on any other layout the pin
resolved empty and the install fell back to the hardcoded "3.14".
setup-hermes.sh's own pin() already established the contract for the
same file ("follow object names and braces, not indentation"); both
sites now follow it. The mirror read is a flat key match; the python
pin uses the same brace-tracking reader as pin().
No other site in the tree has the pattern: setup-hermes.ps1 uses
ConvertFrom-Json, nix uses builtins.fromJSON, python readers use
json.loads, and remaining awk users parse command output, not config.
Tests:
- tests/pm/test_setup_lock_format.py now parametrizes the mirror json
indent (it was fixed at 2, leaving the mirror read unguarded) and
adds a mirror-fallback E2E: the primary url is a dead port (curl
exit 7, retriable), so the staged uv must come from the mirror,
with the mirror json written at 9-space indent. Red on the old
regex (mirror url resolves empty, exit 1), green with the fix.
- tests/test_install_sh_python_pin_indent.py (new) sources
install.sh --manifest and proves bootstrap_python resolves the
pinned version through a fake uv that records its calls, across
2/4/0/tab/blank-line lock layouts.
Verified via scripts/run_tests.sh: 16 passed, 0 failed on the fix; the
new mirror-fallback test fails against the old parsers (verified by
stashing the two script changes and re-running). Sibling install/setup
tests (test_install_sh_node_deps_workspaces, test_install_stage_frames,
test_install_sh_desktop_stage, pm/test_activate_scripts) all green.
Sherpa 1.13.8 supplies native Windows ARM64 Python and core wheels.
Remove its platform exclusion so auto selects the keyless engine there.
Declare pypinyin for both wake install paths. The tokenizer imports it
unconditionally, including for English phrases. Include sentencepiece
in the lazy engine extra too. Keep the matching core in the lockfile
and admit the reviewed release with bounded package cutoffs.
Verified native ARM64 Python 3.14 with the unchanged Hermes engine:
three phrases detected twice each, with no triggers in 180 seconds of
silence. Engine construction failed without pypinyin and passed with it.
The tested wheels match the lockfile hashes. Focused tests: 78 passed,
2 skipped. uv lock --check passed. No MSIX rebuild or microphone test.
The dependency audit reports the same existing httpx2/httpcore2
advisories as the parent commit. No unrelated packages changed.
The fixed openWakeWord default selects an unavailable engine on native
Windows ARM64 and Intel macOS. Use auto and the existing PM platform gates
to prefer openWakeWord, then sherpa, then Porcupine.
Keep explicit provider choices unchanged. Porcupine still requires its
access key, and wake detection remains disabled until the user enables it.
Expose auto in the config UI and document the backend-platform selection.
Verified config loading, platform selection, explicit-provider preservation,
key requirements, and the config schema. No microphone detection was run.
An inherited HERMES_HOME can defeat a test bundle's data-directory suffix.
Older Windows installers also persisted that variable in the user registry.
This gives the app fresh UI state while its backend reads existing sessions.
Add --bundle-unset NAME, encoded as null in the existing bundle environment
object. Apply each clear as an explicit empty value before module startup.
Do not restore an explicitly empty HERMES_HOME from the Windows registry.
Ordinary defaults still preserve runtime overrides.
Verified the release parser, builder handoff, compiled startup ordering,
registry opt-out, and child environment with focused regression tests.
A native Windows probe passed with an inherited home. No MSIX was rebuilt.
Commit builds now show 'Hermes Agent <sha7>' (e.g. Hermes Agent abc1234)
and canary builds 'Hermes Canary' / 'Hermes Light Canary' / 'Hermes Agent
Canary' as the OS-visible product name, so side-by-side installs and
per-commit artifacts are readable at a glance.
Display-only by design: appId, appNamePascal, and msixAppIdWithOrg are
unchanged, so a canary MSIX still updates in place over stable and
userData / single-instance sharing with the stable install is unaffected.
bundle-electron-main.mjs derives the commit from the install stamp
(source='commit-build') so the baked runtime identity matches the
packaging identity.
ntpath.isreserved was added in Python 3.13, but release workflow legs
(commit-builds-summary, builds-table, builds-pending) run bare python3 on
ubuntu-24.04, whose system Python is 3.12. render-builds-table.py crashed
with AttributeError before rendering the expected-binary matrix.
Port the CPython ntpath reserved-name semantics (device stems incl.
superscript COM/LPT forms, trailing dot/space per component) into
_is_windows_reserved() in scripts/releases/r2.py so the release transport
stays self-contained on whatever python3 the runner provides. Verified
byte-parity against real ntpath.isreserved on a 239-case corpus.
Merge ethie/shared-product-builders with the CI dependency cache and native Windows setup work. Preserve UTF-8 diagnostics in the shared Python environment runner. Pass a persistent cache through isolated native staging and PM-runtime construction. Reuse one Windows prerequisite installer from source setup, native adapters, and CI, preserving Rust homes across HOME isolation.
Verified 85 targeted Python tests (5 host skips), 18 JavaScript tests, workflow validation, and scoped lint/typecheck. On native Windows ARM64, five prerequisite contracts passed and the actual shared provider reused OpenSSL, compiled its header with MSVC, and retained Rust under isolated HOME. Full signed distribution builds and live Actions cache transfer remain CI verification.
Standalone Python cannot locate the system CA bundle on this NixOS host.
Use the shell-staged uv to prepare the independent PM runtime before PM
fetches managed Python. Declare and lock truststore in that runtime, then
activate it before CLI and worker imports construct HTTPS clients.
Make setup's awk pin reader follow object nesting rather than indentation.
Use the same reader for tool versions and artifact fields.
Verified cold activation with CA overrides removed, pinned Python and uv
downloads, pm doctor, and a public worker HTTPS install. The targeted suite
passed 56 tests with one Windows-only skip. The TLS regression fails when
truststore is installed but entrypoint activation is removed. Bash syntax
and Ruff checks passed. The full suite was not run.
Two additional setup-toolchain tests fail on unchanged HEAD because their
fixtures reach the real home before home isolation. CI bootstrap unification
is not part of this change.
Source activation could not build cryptography because the setup shell
could not discover the installed OpenSSL development libraries.
Configure Visual Studio ARM64, Clang, Rust, and static OpenSSL before PM
runs. Reuse installed tools and install missing prerequisites. Select a
classic vcpkg with a ports tree and use an explicit installation root.
Report damaged shared libraries without deleting the shared installation.
Verified native PowerShell activation and deactivation on Promise, then
warm activation with no new dependency generation. Cryptography imported
with static OpenSSL. Real vcpkg checks covered installation into a path
with spaces, warm reuse, manifest mode, and damaged-package rejection.
The canonical Windows runner passed the helper and output-encoding tests.
Fresh Visual Studio and Rust installation were not exercised because
Promise already had those toolchains installed.
Windows defaulted captured uv output to CP1252. A UnicodeDecodeError in
the pipe reader hid the OpenSSL build failure and left an empty diagnostic.
Decode uv and npm output as UTF-8, replacing malformed bytes while keeping
the exit status and build error.
Real subprocess tests cover stdout, stderr, legacy locale defaults, and
malformed output. The encoding tests passed on native Windows ARM64 and
Linux through scripts/run_tests.sh.
Build TUI, web, desktop UI and runnable agent products from explicit
prepared inputs. Keep dependency preparation separate from distribution
packaging, with PM and native builds sharing uv environment construction.
Docker copies compiled frontend products instead of build dependencies.
Nix retains uv2nix environments and consumes shared assembly through store
references. Native desktop and Termux use the same launcher and frontend
contracts. Preserve the independent PM runtime and source imports from
arbitrary working directories.
Keep failed frontend builds from replacing the previous product, reject
source/output overlap, and bound dependency-process output draining.
Include hermes_wisdom in the Nix wheel: real CLI smoke tests exposed its
missing package declaration on the base revision too.
Verified focused Python and JavaScript suites, Docker build/runtime checks,
Nix desktop and CLI/ACP checks, standalone TUI and packaged Electron PTY,
and real full-Chromium interaction. Native signed installers, Android device
installation and the full repository suite remain CI verification.
Activation reaches plugin discovery before the application dependencies
exist. Give PM its own locked Python project and runtime so it can install
or repair the application without importing that dependency tree.
Keep PM outside the application workspace. A shared uv workspace resolves
the application graph and cannot provide this isolation. Route mutations
through an isolated worker and preserve transaction callbacks, cancellation,
custom package registrations, and correlated receipts.
Use the same runtime builder for source installs and packaged payloads.
Keep offline wheelhouse support in that builder. Nix builds the independent
PM lock as a separate derivation. Refuse lazy-disabled bootstrap before
installing tools or dependencies.
Move first-party YAML readers and writers to ruamel. Keep the application
lock's transitive PyYAML requirements for third-party packages.
Verification:
- Focused canonical Python suite: 177 passed, 1 host-gated skip.
- Electron backend probes: 12 passed. Electron typecheck passed.
- Both uv locks, scoped lint, Bash syntax, and whitespace checks passed.
- Cold activation, corrupt-app repair, offline staging, and relocation ran.
- Built and exercised the Nix PM runtime and standalone YAML merge script.
Six broader caller test files retain the same 24 failing test IDs as an
archive of HEAD. The existing real-home guard blocks those tests before
they can exercise the affected paths. No full-suite pass is claimed.
Native Windows signing and full Bionic package execution remain unverified.
The merge combined old callers with newer lifecycle and update modules.
It also dropped native handlers while keeping their preload methods.
Type declarations alone could not repair those runtime failures.
Restore bounded backend teardown and retain failed-stop ownership.
Use API-only passive checkout checks with a daily disk cache, and pass
manual refresh requests through the updater strategy. Keep the shared
About UI and restore onboarding, feature flags, and notification wiring.
Verified all workspace typechecks, lint on the changed desktop files,
focused UI and Electron tests, and the development bundle. A headless
Electron smoke test exercised the real main process, preload, and native
IPC. A separate test exercised update checks with a linked git worktree,
loopback HTTP, and the disk cache. The full repository suite was not run.
The _sql_json_extract wrapper removed the `COALESCE(model_config` text the
alias string-replace keyed on, so the deferred-backfill SELECT joined an
unqualified `model_config`. Build the predicate from the alias directly and
derive the unaliased constant from it, so the two can never disagree.
Sibling widening of the #101726 salvage: FTS_TRIGRAM_SESSION_SQL (trigram view/triggers/backfill),
the v16 delegate-tagging data migration and reopen_session's legacy reset-child stamp still called
json_extract() on the raw model_config cell, so one malformed JSON row could still abort FTS
maintenance, a schema migration or /resume of a reset child. Zero raw model_config json_extract
reads remain in hermes_state_*.py.
Three invariants over real SQLite fixtures (TEXT and 8.4e252 timestamps written straight into the
REAL columns; 1200 ids under a 999-variable ceiling via setlimit): list/export/insights complete
and name the corrupt session in a WARNING; writers never persist an out-of-window timestamp;
prune/delete_sessions succeed with zero orphaned messages. All three fail on origin/main.
prune_sessions(), delete_empty_sessions() and prune_empty_ghost_sessions()
built one IN (?, ..., ?) clause containing every selected session id (the
parent-orphaning UPDATE), so cleaning more than SQLITE_MAX_VARIABLE_NUMBER
sessions failed atomically with "too many SQL variables". The per-row
DELETE loops that followed are folded into the same 900-id batches.
Same single _execute_write() transaction; only the binding is split.
Hand-ported from PR #100658 (targeted the pre-decomposition hermes_state.py
god file; the methods now live in hermes_state_maintenance.py /
hermes_state_sessions.py). The one-pass transcript-directory sweep from
that PR is not ported (out of scope for the variable-limit bug). Authored
by @Mi55ed; ported under --author.
`hermes sessions prune --source cron --older-than 14` on a store with ~60K
cron sessions (~50K matches) died with sqlite3.OperationalError: too many
SQL variables. SessionDB.delete_sessions, _collect_delegate_child_ids and
_delete_delegate_children each bound the full id list into a single
IN (?,?,...). SQLite caps bound parameters at SQLITE_MAX_VARIABLE_NUMBER
(999 on < 3.32, 32766 after), so any bulk delete above that failed outright.
Chunk every IN list (`_id_chunks` / `_SQL_IN_CHUNK` in hermes_state_common,
900 ids; the delegate walk binds each id twice so it chunks at half). Same
transaction, same cascade/orphan contract; only the parameter binding is
split.
Hand-ported from PR #102679 (targeted the pre-decomposition hermes_state.py
god file; the functions now live in hermes_state_sessions.py). Authored by
@mssteuer; ported under --author.
SQLite dynamic typing lets a TEXT cell ('not-a-timestamp'), inf/nan or a
garbage double (8.4e252 salvaged from a damaged page) sit in a REAL
timestamp column. Every reader called datetime.fromtimestamp()/float
arithmetic on the raw cell, so ONE bad row raised TypeError/OverflowError
out of the row loop and took down the whole `hermes sessions list`/browse
table (#102399), all three exporters — JSONL/MD, QMD, HTML (#102352) —
and `hermes insights` (#99959).
Fix the class with ONE helper, hermes_cli.timefmt.coerce_epoch(): a
stored cell becomes float epoch seconds inside a sane 1970..2103 window
or None after a WARNING that names the session id. Every reader routes
through it — relative_time (list/browse/resume picker), format_epoch
(prune/candidates tables), the three exporters' timestamp formatters,
insights' _get_sessions/_day/period range — so a bad row renders as
'?'/'N/A'/raw text for that one cell and the command completes.
Write side: hermes_state_messages._coerce_timestamp (append_message,
append_messages_batch, import) and the import path's started_at now use
the same window, so a new out-of-range timestamp falls back to now()
instead of being persisted — new bad rows cannot be written by Hermes.
Reported-by: #102399, #102352, #99959 reporters; kokhlo's insights
analysis pointed at every reporting site, not just line 860.
The schema initializer now replays SCHEMA_SQL through executescript (the
single-authority path from #94701's follow-up), which bypasses the
execute()-level DDL failure injection — the regression stopped raising.
Bind the same simulated failure onto the executescript path so the
connect-close-on-init-failure contract stays pinned for both replay
mechanisms.
Review follow-up on #94701: the delegation tool's _initialize_schema
still carried its own CREATE TABLE + ALTER column list for
async_delegations, leaving a second durable-shape authority even with
the column declared in SCHEMA_SQL. Its legacy ALTER added
origin_session_id as bare TEXT (nullable, no default); reconciliation
repairs missing column names only, so a database first opened through
the tool kept a non-canonical shape forever (#94691).
Remove the private DDL entirely. The tool's initializer now calls a new
reconcile_state_schema() in hermes_state_schema, which replays the
canonical SCHEMA_SQL (idempotent CREATE IF NOT EXISTS for every table,
canonical indexes included) and reuses SessionDB's declarative
_reconcile_columns for missing-column backfill — one reconciliation
implementation, one authority. Because _parse_schema_columns
reconstructs each column's full constraint expression (type, NOT NULL,
DEFAULT), the tool-first legacy path now adds origin_session_id as
TEXT NOT NULL DEFAULT '' — the canonical shape — and SQLite backfills
existing rows with the '' default.
Opening-order regressions compare FULL PRAGMA table_info metadata
(type, notnull, dflt_value, pk) plus the canonical index set across
fresh SessionDB→tool, legacy→SessionDB, and legacy→tool→SessionDB,
each preserving a pre-existing legacy delegation row.
The delegation tool carries its own CREATE TABLE for async_delegations
(tools/async_delegation.py _initialize_schema) plus a lazy ALTER TABLE
ADD COLUMN for the tables it finds already existing. Its column list
had drifted ahead of the canonical SCHEMA_SQL: origin_session_id
(raw api_server session id of the originating request, the wake
self-post target) existed only through the tool's lazy path, so two
databases at the same schema_version had different
async_delegations shapes depending solely on whether the delegation
tool had ever run. Rebuild/replay pipelines that reconstruct state.db
from the canonical schema then hit the column with no version gate to
explain it (#94691).
Declare the column in SCHEMA_SQL with the same TEXT NOT NULL DEFAULT ''
shape the tool uses. Fresh installs now carry it canonically; the
declarative _reconcile_columns backfills it into legacy databases on
the next writable open (same pattern as earlier additive columns); the
tool's lazy ALTER keeps serving pre-reconciliation databases. The two
schema authorities now agree, pinned by a test that runs the tool's
initializer over a canonical database and asserts the shape is
unchanged.
Fixes#94691
BtbN publishes new FFmpeg builds without changing the version number.
The shared-minor comparison therefore reported stale artifacts as current.
Compare advertised artifact URLs during resolution and hash changed URLs
when applying the update. Keep dry-run checks metadata-only and preserve
pins for targets without an update source, including Termux.
Verification: 41 focused tests passed. The regression exercises real
archive downloads, installation, retained target pins, and a second
update that performs no writes. Native ARM64 FFmpeg also passed a real
16 kHz audio encode after installation.
The corruption explainer filled `{db_path}` from `_default_db_path()`, the
process default. A Desktop `serve` backend launched on the root home hosts
named-profile sessions whose SessionDB is `profiles/<name>/state.db`, so the
operator was told to inspect/repair a different profile's database. Pass the
agent's own `_session_db.db_path` from the turn finalizer; the process
default remains the fallback for agents without a bound store.
Reported in #105887.
Reading a session by id that missed the caller's store fell through to
_locate_session_db(), which opened every profile's state.db read-only and
returned the first owner's full transcript — no opt-in, no profile named, and
the miss path even fired after an explicit non-matching profile= read. Any
caller holding an id (ids appear in logs and tool output) could read a
foreign profile's conversation. Profiles are isolated islands by design.
A miss now stays a miss, with a hint to name the owning profile
(profile=<name> / @session:<profile>/<id>), which remains the sanctioned,
explicit cross-profile read. The schema eval runner no longer needs to fake
the scan.
Reported by the #106761 filer; reproduced by @kokhlo. Refs #87779.
The salvaged #102534 test patched hermes_state.get_shared_session_db, a seam
main dropped (server._get_db now calls hermes_state_registry.acquire), so the
fixture errored at setup and the file reported 0 passed. Assert on the real
handle's db_path and on the foreign home staying untouched instead of on a
fake factory; red with the pin reverted, green with it.
The lazy _get_db() singleton followed get_hermes_home(), so a first touch
inside the multiplex cron ticker's per-profile override window permanently
bound the default backend to another profile's state.db (#102526).
close_all_under returned after the last release dropped the generation
and before the physical close finished, so rmtree still saw the open
handle. Wait directory-matching teardown barriers the same way close_all
does.
delete_profile already force-closes holographic memory_store.db in this
process, but the shared SessionDB registry kept state.db open. Recreate
then failed with a replaced/locked database. Close every shared handle
under the doomed directory, same contract as MemoryStore.release_all_under.
Co-authored-by: Cursor <cursoragent@cursor.com>
Since 0.21.0 reads go through mode=ro pooled connections. A read-only OPEN already
rides out the millisecond WAL transition window (checkpoint / WAL reset / frame flush
by a sibling process; the ro reader cannot rewrite the -shm index) with a bounded retry
(#100436), but a WARM pooled reader hitting the same window while its SELECT executes
propagated `disk I/O error` straight out of get_session(): 37 identical tracebacks on a
multi-process WSL2 ext4-on-vhdx install, each followed by "compression session recovery
failed", with quick_check=ok (#100871). The reporter's A/B shows the operator
workaround (journal_mode=delete) collapses read throughput ~30000x, so the flake has to
be absorbed on the read path.
_read_one/_read_all now replay the idempotent statement within the existing read-only
IOERR budget (3 x 50 ms) on the SAME connection -- close+reopen would cancel this
process's POSIX locks for every sibling connection -- and a persistent IOERR still
propagates. No quarantine: EIO on a read is busy, not broken. Every SELECT in the
SessionDB siblings (63 call sites) reaches the pool through these two helpers, so the
class is covered without a wrapper type.
Same-connection retry per #100882's analysis (@fangliquanflq); #100883
(@Sahilvishnaliya) diagnosed the missing recovery in the 0.21.0 read pool.
Fixes#100871.
Co-authored-by: fangliquanflq <fangliquan@qq.com>
Co-authored-by: Sahilvishnaliya <222165401+Sahilvishnaliya@users.noreply.github.com>
apply_wal_with_fallback() reports "wal" in two indeterminate cases -- the vulnerable-
SQLite gate (_apply_delete_for_wal_reset_bug) and the non-vulnerable probe-unknown path
(a7f2a593d1) -- meaning "touched nothing, the connection inherits the header's mode".
SessionDB turned that assumption into `_wal_active=True`, which enables the mode=ro read
pool that skips `self._lock`. On a file that is really in rollback-journal mode those
readers race the writer with a 5s busy timeout and no retry: random SQLITE_BUSY read
failures for the instance's lifetime (#86515).
Confirm the header on the freshly opened connection before enabling the pool. When the
probe is still blocked, reads queue on the writer connection under the lock -- slower,
never wrong. Every other apply_wal_with_fallback caller ignores the return value, so the
consumer is the right place to gate; changing the return contract to Optional across
15 call sites (#87044's shape) is not needed.
Live repro: DELETE-mode file, sibling holding BEGIN EXCLUSIVE during open ->
before: _wal_active=True and _checkout_read_conn() hands out a pooled mode=ro conn;
after: _wal_active=False, reads take the locked writer path.
Fixes#86515. Based on the analysis in #87044.
Co-authored-by: QDung210 <dqdung205@gmail.com>