What: platform_default_home() appends HERMES_DATA_DIR_SUFFIX literally to the hermes/.hermes leaf via platform_default_home_with(suffix), which fixes hermes_home(), hermes_root() and the update marker together.
Why: Python (_get_platform_default_hermes_home) and Desktop (platformDefaultHermesHome) both append it, so a hermes-setup launched in a suffix-only channel environment read and wrote the unsuffixed home and marker.
Red: paths::tests::data_dir_suffix_names_the_default_home_and_its_marker_root on the 0ec53574011 resolution (byte-verbatim extract, suffix ignored): left '.hermes' right '.hermes-channel-build-x'.
Green: passes on the real fixed source extract; also cargo check clean for x86_64-pc-windows-msvc (Tauri crate itself cannot build on this host).
What: the Windows pid_is_alive opens the process with SYNCHRONIZE and returns WaitForSingleObject(h, 0) == WAIT_TIMEOUT; access denied still reads alive via liveness_from_open_error.
Why: GetExitCodeProcess == STILL_ACTIVE (259) is also what a process that exited with code 259 reports while any handle keeps its object, and its creation time still matches the v2 marker, so Busy had no age bound. Python's _early_recovery already uses the wait probe.
Red: Windows-only; marker_tests::pid_is_alive_false_for_an_exited_process_whose_exit_code_is_259 (cfg(windows)) needs a native runner.
Green: cargo check --tests --target x86_64-pc-windows-msvc clean on the real marker.rs (std-only harness); Linux marker tests 49 passed.
What: the Python and Rust tmp sweeps reclaim a tmp sibling named for our own pid once its mtime is older than 60 s (OWN_PID_TMP_STALE[_SECONDS]); other pids keep the liveness check, and an ASCII pid past u32 is dead in both (Python had kept it since N06; Rust always swept it).
Why: with deterministic container pids a previous holder of our pid number left a tmp that every later run skipped as 'our own in-flight write', so it was never cleaned. Every write of ours finishes far inside 60 s, so a fresh same-pid tmp is still kept.
Red: test_stale_tmp_under_our_own_pid_number_is_reclaimed FAILED and marker_tests::stale_tmp_under_our_own_pid_number_is_swept FAILED on 07c014355b5 (real marker.rs via a std-only harness; the Tauri crate cannot build here without webkit2gtk/dbus).
Green: both pass; test_update_lock.py 59 passed/1 skipped; marker tests 49 passed/2 ignored.
What: every Windows process that joins its ancestor's checkout lock also locks one of 16 lease bytes
past the owner's byte, taken while the owner is seen alive; _try_lock/checkout_lock_held and the Tauri
marker probe treat any held lease as a held lock. The refused completion child still runs (D2: a
committed update never fails) but is no longer unfenced.
Why: a child outside the kill-on-close job outlived a killed owner, whose msvcrt lock died with it,
so a contender acquired while the child still wrote.
Red/green: Windows-native cells (Python owner-killed/unbound child; Rust held lease) need a native run;
POSIX paths unchanged (update_lock suites pass, same failure set as base).
UpdateMarkerGuard::acquire now takes the install root. Under the marker mutex, a dead marker is
kept (never compare-and-deleted) while the install's checkout kernel lock is held, mirroring
hermes_cli/update_lock.py: checkout_lock_path (common git dir or <root>/.hermes-update.lock) and
checkout_lock_held (POSIX flock probe; Windows LockFileEx on the byte at 1<<20 that msvcrt locks).
The refusal is Busy with a held owner (pid 0), and busy_update_message says the owner exited but a
process it started still holds the checkout. should_heal_self_marker_refusal no longer heals an
exit 2 while the checkout lock is held.
Windows pid_is_alive treated every OpenProcess failure as dead, including
ERROR_ACCESS_DENIED for an elevated / other-user updater, while Python's
_early_recovery._pid_is_running calls it alive (racing an elevated updater is
worse than postponing). The decision is now the pure
liveness_from_open_error(GetLastError()): access denied = alive, any other
failure = dead. The creation-time probe still returns None for such a pid, so
the combined verdict equals _identity_live: live only within the v1 age
ceiling of started_at.
The new oversized-numeric corpus cases already pass in Rust (u32/u64 parse
failure = malformed, saturating age, +inf ct never matches); unit tests now pin
the started_at = u64::MAX age (0, no overflow) and the +inf ct explicitly.
Release now judges the marker by identity, and the identity check sent
os.kill(own_pid, 0) on every release. Our pid is alive by definition; only
the incarnation (5 ms ct) is in question. Same short-circuit in marker.rs.
R3 / A7 rule 1: every marker decision-and-mutation (reclaim of a dead claim,
adopt + delegate append, release, ownership hand-over) now runs while holding
the kernel lock on the never-deleted sidecar `<marker>.lock` (POSIX flock;
Windows: the file opened share-none, the same lock the PowerShell hand-off
takes), bounded to 10 s. Only the exclusive first publish stays lock-free
(rule 2). Python (update_lock.marker_mutex) and Rust (marker.rs lock_marker)
interoperate on the same lock. A stale reader paused before its unlink can no
longer delete a claim published after its read.
A7 rule 4 / R12: one incarnation rule. Our own pid is ours only within 5 ms of
our creation time; a no-ct claim naming our pid is a previous incarnation
(dead) — in Python (`update_lock.incarnation_live`, exported for the pause
record) and in marker.rs (no more 2 s self tolerance; Electron's v1 own-pid
pre-write is reclaimed and republished as v2; the exit-2 heal requires our
exact incarnation).
A7 rule 5: release is identity-based (`_release_decision` / `release_decision`).
The owner removes its claim regardless of delegate lines unless a LIVE
delegate other than itself exists, which then inherits the claim (rewritten
with the delegate as owner, started_at and run lines kept). A delegate drops
its line while the owner lives, else deletes. This is the native Windows
`orphaned_update` root cause: the hand-off script named `hermes update` as
delegate before it claimed, the child adopted without writing, and its exit
released nothing, leaving '<dead owner> ... delegate:<exited child>'.
Parser (A7 rule 7): lines 4+ are tagged — first well-formed delegate line,
every `run:<id>` line; pid must fit u32. Both consumers run
tests/fixtures/update_marker_corpus.json (judge + release cases).
R2: checkout mutators keep custody. `_git_run` passes the held checkout lock
fd to git (and its hooks); `run_source_script` passes it to the Node build;
`complete_source_checkout` ACQUIRES `UpdateLock(install_root=root)` instead of
claiming only its home's marker; venv_sync's interrupted-update finish acquires
the checkout lock before mutating instead of sampling it and hands the fd to
its completion child; the historical takeover chain passes it down. On Windows
a child of the update joins the lock when its holder record names a live
ancestor (it runs in that owner's kill-on-close job).
R11: a live pre-root-marker claim under <root>/profiles/*/ (the old per-profile
marker) is observed: a new update refuses while one is live and not a partner.
Real-process tests: tests/hermes_cli/test_update_lock_protocol.py (each fails on
6ff4055cd74), corpus consumers in Python and cargo.
The Rust updater now applies the same marker rules as hermes_cli/update_lock.py:
- A2 parse: one BOM and one trailing CR per line are dropped, spaces/tabs
trimmed; lines 1-2 must be ASCII digits (fractional/garbage => malformed,
dead, compare-and-deleted); line 3 is `ct:` or a v1 marker; only line 4
can be `delegate:<pid> ct:<ct>`.
- A1 liveness: a live pid whose creation time cannot be read gets the v1
20-minute ceiling instead of an unbounded pass. The ct probe is injected
so the rule is unit-testable.
- A3 claim: the body is written and fsynced in a `<marker>.<pid>.<n>.tmp`
sibling and published with an exclusive hard link (fallback: create_new);
a 0-byte marker younger than 5 s is a claim in flight (live, never deleted).
- M1: an own-pid v1 pre-write (Electron) is CAS-upgraded to v2 with our
creation time, lines 1-2 byte-identical, and released by complete().
- m8: a marker that cannot be written refuses the update with a clear
message instead of running unclaimed (AcquireError::{Busy, Unwritable}).
- m10: dead claimants' tmp siblings are swept at claim time.
Pure move of UpdateMarkerGuard, the marker parse/liveness/compare-and-delete
helpers, the exit-2 self-heal decision and their tests out of update.rs
(2514 lines) into a new marker module. No behaviour change; update.rs now
imports the guard and the heal decision from crate::marker.
UpdateMarkerGuard now implements update-marker contract C1:
- marker lives under the profile-tree root (paths::hermes_root), never profiles/<p>
- body pid / started_at / ct:<creation time>; GetProcessTimes on Windows,
/proc stat on Linux, proc_pidinfo on macOS
- an owner is live iff its pid is alive and its creation time matches (2.0 s);
no age ceiling for v2 markers, the 20-min ceiling only ages out v1 markers
- claim is create_new + sync_all with one compare-and-delete reclaim of a dead owner
- every delete is compare-and-delete; a live line-4 delegate keeps the marker live
Why: a 20-min ceiling reclaimed live owners mid-update, a reused Windows pid held
the gate, and read-then-write claims let two updaters run at once.
Tauri's bundler (cli 2.11.4) compiles an Icon Composer package listed in
bundle.icon into Assets.car with actool >= 26 and sets CFBundleIconName, so
the bootstrap installer gets an unbranded twin of the desktop package under
src-tauri/icons/icon.icon (generator target; the bundle stays the plain
brand because flavours only apply under apps/desktop/). Tauri only logs when
actool is too old, so the macOS job selects Xcode 26 and the verify step
asserts Assets.car and the plist key are in the built .app.
The desktop DMG's volume icon defaulted to the packager's icns, which is
actool's 256px fallback whenever mac.icon is the .icon package; point
dmg.icon at the full-resolution assets/icon.icns. Tauri's DMG already takes
--volicon from the bundle's .icns, which the regenerated outputs cover.
Next to macOS 26's own icons the flat plate + girl reads better than the
ringed tile, so the ring is off in every output: the tiles' inward stroke,
the layered icon's border layer and its targets. BORDER_ENABLED keeps the
ring code one flag away.
Without a ring the girl's lowest nodes are dragged past the tile edge
(EDGE_OVERSHOOT) so the outline crops her instead of leaving a gap.
Clear light/dark and Tinted light/dark now come from one grayscale mono
layer with Liquid Glass on: the parts of the girl that are black in light
mode become a near-black frosted ink (0.13 @ 90%), her white parts stay
white, each pixel painted exactly once so materials never stack. The art
layer hides under "tinted" and the mono layer shows only there.
Mac girl at 1.06x (between main's 1.12x and the layers' 1.0x), for the icns
and the layered icon alike. Chosen from a six-appearance matrix of real
IconServices renders next to Finder.
Rust live_marker_owner now REMOVES a stale marker (dead pid, past ceiling,
unparseable) on read — previously stale bytes were only ignored, so a crashed
updater whose Drop never ran refused every later acquire until the 20-minute
ceiling (#77259). pid_is_alive gains pid-0 and zombie handling (Linux /proc
state, macOS ps stat), closing the kill(pid,0) false-positive that kept a
dead-but-unreaped updater 'live'.
Python _pid_is_running gains the same zombie awareness via a new
_process_state probe (Linux /proc, macOS ps), which read_live_update inherits
through update_lock._pid_alive — so the CLI gate self-heals a zombie-owned
marker in seconds instead of 20 minutes (#120635, #125932).
Electron readLiveUpdateMarker consults a new posixProcessState probe
(injectable, fail-open to the signal-0 verdict) so the desktop boot gate no
longer parks on a zombie-owned marker.
Rust fix cherry-picked from #77885 (authorship kept); Python and Electron
are new parity work.
Hermes-Setup.exe replaced install.ps1's .hermes-bootstrap-complete with its
own copy: completedAtUnix instead of the completedAt every other writer and
reader uses (install.ps1, Electron main, hermes_cli/source_stamp.py,
scripts/verify-bootstrap-version-stamp.py), and a pinnedCommit from a bare
`git rev-parse HEAD`, which is null on a machine where git exists only as
the PM-staged binary install.ps1 uses.
The receipt now carries completedAt (UTC ISO-8601, milliseconds), and the
commit resolves like install.ps1's Stage-Complete: the pinned commit, else
the checkout HEAD read from its ref files (loose ref or packed-refs), else
the full sha install.ps1 already recorded (read BOM-tolerant).
Fixes#124949
resolve_hermes_desktop_exe and the update lock probe only knew the x64
linux-unpacked dir, so on ARM64 Linux the bootstrap installer could not
find the rebuilt app to relaunch and did not wait for the running app's
app.asar before an update. Add linux-arm64-unpacked to both lists.
Dev runs replace the Dock icon with public/apple-touch-icon.png, which the
generator rendered full-bleed, so it drew ~24% larger than its Dock
neighbors. Render it from the mac-grid master like the icns targets.
On the 824 grid the plate matches peers, but the girl inside a white tile
with a ring read small; scale her 1.12x about the plate center for every
mac target.
The setup launcher had no LSUIElement, and its already-installed hand-off
ran after Tauri/AppKit, whose default activation policy is Regular. That
registered the setup bundle as a second Dock app beside the real desktop.
Hand off before constructing Tauri, and restore Regular activation only
when the installer UI is actually shown.
On macOS the Electron single-instance lock also ran before deep-link
registration. setAsDefaultProtocolClient relaunches the app through Launch
Services, so the loser already had a Dock icon by the time the lock failed
and app.exit(0) ran. Register the protocol first. The lock-losing instance
still hard-exits before ready.
Fixes#73151
Every bootstrap downloads the script fresh, commit pins included; the
on-disk file is only the -File target for this run. Removes the reuse
path, ScriptSource::Cached and the in-place BOM upgrade of old caches.
A 429 from raw.githubusercontent.com made the installer fall back to a
cached pre-migration install-main.ps1. Its repository stage then
fast-forwarded the checkout to live main, whose lock only supports
Python >=3.14, so the old script's 3.11 venv got no core dependencies
and every install died at 'Baseline imports failed'.
The checkout follows the live branch, so the script must too: a failed
refresh of a mutable pin is now fatal (Retry re-downloads). Immutable
commit pins keep permanent cache reuse.
User installs failed with 'resvg-py is missing' because the web/desktop
source builds rendered icons on whatever python was on PATH. The default
brand outputs are now committed; source_build, apps/desktop build.mjs and
the npm/docusaurus pre-hooks consume them directly. Flavored release
bundles (canary/commit) still render into their own product dir.
icons-freshness-check now regenerates and fails on any byte diff.
Every exit 2 from `hermes update` was reported as "Hermes is still
running. Close all Hermes windows", including when the holder was another
live update. The child prints the specific refusal as a ✗ block right
before exiting; keep a bounded stdout tail and show that block, falling
back to the generic text only when none was captured.
Co-authored-by: RelaxJonh <92573950+RelaxJonh@users.noreply.github.com>
The committed version is a placeholder now; a build stamps the real one.
Dev installs report their distance from the highest reachable release tag,
so an -rc claim sitting on the same commit never becomes the version.
Reconcile plugin declarations and validation through PM's atomic generation publication; preserve external runtimes, target markers, and conflict refusal. Keep one source-update completion owner and port upstream lifecycle changes to the PM desktop/runtime paths.
Only Log lines were sanitized at the emitter; the manifest-step error string
was still built from the raw child stderr/stdout tail, so a failing
`install.sh --manifest` (print_error writes `${RED}✗${NC}`) put escape bytes
straight into the Setup failure banner. Run stripAnsi / strip_ansi over the
embedded tail on both the Electron and the Tauri bootstrap surfaces (#112675).
Seven near-identical #[test] fns become one table test over the same inputs
(SGR banners, cursor/erase/private modes, OSC with BEL and ST, \r redraws,
sequences cut by the pipe, plain multi-byte text) plus the existing
sanitized_for_ui_only_touches_log_lines. Same coverage, two tests: the
salvage bar is ≤2 invariant tests per fix.
Verified with the function and tests extracted into a standalone
`rustc --test` binary (cargo test on this host fails in libdbus-sys, a
missing system header unrelated to this crate's code).
The macOS setup app drives a shell install script whose output carries
SGR styling, cursor movement, and OSC title commands; the Live output
pane renders log lines as plain text, so those bytes showed up as
mojibake (#112675).
Strip escape sequences at the Rust-to-webview event boundary (bootstrap
and update flows) and collapse carriage-return redraws to the last
visible frame. The on-disk tee keeps the raw bytes for terminal viewing.
Fixes#112675
Trim the Windows-only re-exec matrix from four tests to two: one per
launch builder (std = launcher fast path, tokio = `--update` handoff),
each paired with a different captured stream. stdout and stderr flow
through the same handle-inheritance path (GetStdHandle +
SetHandleInformation on the installer side, Stdio::null() on the child),
so the 2x2 product added runtime without adding a distinct invariant.
Also replace the "(issue TBD)" placeholder on open_macos_app_detached
with the issue number.
`hermes-setup.exe --update` hands off to the rebuilt Desktop right before it
exits. On Windows, CreateProcess duplicates every inheritable handle of the
installer into the Desktop, and the installer's stdout/stderr are inheritable
pipe handles whenever a shell captures its output. The Desktop therefore held
the pipe's write end and the shell kept waiting until the Desktop exited, even
though the update had succeeded (Windows 11, 2026-09-05: still waiting after
18 minutes, while `> file` returned normally). DETACHED_PROCESS detaches the
console only; Rust's std::process passes bInheritHandles=TRUE and has no
handle list (rust-lang/rust#54760), and setting the child's own stdio to null
does not change which stray handles it inherits (measured: same 4 s stall).
Right before the handoff, clear HANDLE_FLAG_INHERIT on the installer's own
std handles (GetStdHandle + SetHandleInformation, best effort, skipping
missing handles of a GUI-launched installer), through one spawn helper used
by both launch sites; the Desktop's stdio is nulled as well so it never uses
the installer's handles at all. The installer's tracing goes to a file, so
nothing is lost. macOS `open` branches get the same nulling for consistency.
Tests (Windows-only; the crate's CI lane runs on Ubuntu): the test binary
re-executes itself as a helper that launches a 4 s sleeper grandchild through
each real builder and the shared spawn helper; the parent asserts the helper's
piped stdout, and separately stderr, reaches EOF in under 2 s and that the
helper's sentinel line arrived. Without the fix all four cases stall ~4 s;
with it, EOF arrives when the helper exits.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Competing installers and checkout-local venv assumptions bypassed PM
selection, install consent, and generation lifetimes. Route consumers
through PM and installation-bound launchers. Refresh source launchers
before obsolete Python entries can be collected.
Remove Node, browser, and CUA acquisition engines, obsolete venv-holder
handling, detached sync, and unused PM APIs. Keep historical updater
exports inert and preserve external tool ownership and native integration.
Share product freshness and prepared inputs across builders. Align plugin
admission, Docker provisioning, setup instructions, and behavioral tests.
Verified targeted Python and JavaScript tests, desktop and web typechecks,
scoped lint, real product builds, and the Docker frontend smoke test.
The missed post-setup test cleanup is included and verified.
Native Windows/macOS execution, full Rust compilation, and the complete
repository suite remain unverified. Historical compatibility requirements
were preserved and extended, not fully rescanned.
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.
- website wordmarks (logo.png/logo-dark.png) drop the black/white frame:
the girl alone on transparency, black for light navbar / white for dark
(docusaurus srcDark stays)
- BrandMark marks are now the app-icon squircle itself (transparent
corners, 824px safe-zone composition) instead of plain tiles; the
components no longer paint a tile/rounding
- generated icon outputs are NOT committed anymore: all 35 targets are
gitignored and regenerated on demand by the consuming pipelines via the
new scripts/generate-icons.mjs (website prebuild, desktop prebuild+dev,
installer prebuild, web prebuild)
- freshness lane switched from byte-compare to structural verification
(sizes, squircle corner transparency, ICO frame sets via header parse) —
no more windows/ubuntu byte drift dance
- marks saved as 8-bit palette PNGs (FASTOCTREE quantize keeps per-index
alpha tRNS -> 2.5KB, AA edges preserved); compose_svg renders in the
background's native space so resvg scales per output size
Refresh the 35 generated targets from the ubuntu freshness lane (the
windows regen compresses PNG differently per host zlib; CI checks
against linux bytes). Girl marks + masters are host-stable (stored
deflate blocks / plain text). Removes the temporary artifact-harvest
step now that the committed set matches the check lane.
The icon pipeline now composes all 35 targets from two source axes instead
of a single hand-edited master:
- girl art: assets/nous-girl-black.svg / nous-girl-white.svg (brand kit)
- backgrounds: assets/backgrounds/ (squircle light/dark, logo frames,
BrandMark tiles)
assets/icon-master*.svg are generated artifacts (squircle background + girl
nested in the 824px HIG content safe zone). New dark-appearance artifacts
(icon-dark.* containers, appx *-dark logos, logo-dark wordmark,
nous-logo-dark) land everywhere a surface consumes them: docusaurus navbar
srcDark, BrandMark dark tile (keyed off renderedMode).
nous-girl.jpg jpgs are replaced by lossless 8-bit palette PNGs saved with
compress_level=0 (stored deflate blocks, byte-identical across hosts for
the CI freshness lane).
Includes the temp linux-truth harvest step in icons-freshness-check.yml so
the committed compressed bytes can be refreshed from the ubuntu regen.
resvg-py 0.4.0 rasterizes identically per-host but the committed set was
generated on Windows (22440-byte icon.png) while the freshness lane runs
on linux (24200). Harvested linux regeneration via a temp artifact step,
committed it (all 23 targets now match linux output), reverted the temp.
generate_icons.py --check was RED on 23 targets — committed files
diverged from icon-master.svg output (large stale binaries: logo.png
1.3MB→60KB, icon.icns 1.5MB→94KB). Regenerated via the single-master
pipeline; --check now passes. Also gates the icons-freshness-check CI
lane that ci.yaml calls.