The splice hazard the tail seed guards against is live in remote-lifecycle's
scrapeReadyPort: the remote spawn log merges stdout and stderr, yet it was
matched with the `^`-anchored READY_RE. Export one READY_IN_MERGED_OUTPUT_RE
(lookbehind token boundary — the hand-rolled `[^0-9A-Z_]` class admitted
lowercase-glued tokens) from backend-ready.ts and use it for both merged
buffers. Comments trimmed to the WHY; 6 new tests collapsed to the two
invariants (splice recovers, prose does not match) plus one remote-log case.
Both callers build the wait in the same synchronous block as
`outputTail.attach(child)`, and stream 'data' is asynchronous, so
`bufferedOutput()` is always empty at the seed and this block recovers
nothing today. Say so at the code, so nobody reads the seed as the live
recovery path for a lost READY sentinel — and so the condition that makes it
load-bearing again (any await reintroduced between attach and this call, the
pre-#100442 ordering) is written down next to the code that depends on it.
The spawn-time output tail feeds stdout AND stderr into ONE buffer, so its
contents are not line-accurate. uvicorn logs to stderr in raw chunks, and a
chunk that ends without a newline is concatenated directly onto the stdout
sentinel that follows it:
INFO Started server process [4711]HERMES_BACKEND_READY port=65238
The late-attach seed scan added for #60323 matched that buffer with the
line-anchored `_READY_RE`, so `^` never lined up and the seed silently
recovered nothing. The live stdout listener cannot cover the gap either: the
sentinel was already consumed by the tail, and flowing-mode streams never
replay chunks to late listeners. The wait then runs out its full 90s deadline
and a healthy, listening backend is SIGTERMed — while desktop.log, which reads
both streams, plainly shows the READY line. That contradiction (READY logged,
boot timed out anyway) is the reported macOS regression.
Scan the tail with a boundary-guarded pattern instead of a line-anchored one.
`port=<digits>` keeps the token unambiguous, so prose naming the sentinel and
longer identifiers ending in it still do not match. The live per-line scanner
keeps `^`: individual stream chunks ARE line-accurate there, and loosening it
would let unrelated child output settle the boot on a bogus port.
Scope: the seed only. Watching stderr on the live path is #97086's change and
is deliberately untouched here — the two compose.
`persistSshConnectionToken()` writes the per-serve session token adopted for
an SSH connection onto its v2 registry entry, but the registry never read it
back: `normalizeRegistry()` rebuilt an `kind === 'ssh'` entry from
`normalizeSshConfig()` alone, which describes only the DIAL (host, user, port,
keyPath, remoteHermesPath, remoteProfile). The sibling remote/cloud branch
preserves `entry.token`; the ssh branch did not.
The token therefore survived only in the mtime-keyed in-process cache. On the
next cold read — an app restart, or any process that re-parses
connections.json — it was silently dropped, so `resolveRemoteBackend()`
decrypted an empty value and dialed with `reuseToken = ''`. That fails the
`Boolean(reuseToken)` clause of remote-lifecycle's `reusable` gate, so a
HEALTHY owned backend was classified not-reusable, reaped by `cleanupStale()`
and respawned on a new port behind a new tunnel — while the renderer kept
dialing its cached `wsUrl?token=` at the old credential and got 403 forever.
`normalizeConnectionInput()` had the same omission: `saveRegistryConnection()`
resolves the surviving envelope via `resolvePersistedRemoteToken()` and passes
it in, but the ssh branch dropped it, so a plain label rename wiped the live
backend's reuse credential and re-armed the same loop.
Both branches now carry the token exactly the way the remote branch does.
There is no auth-mode choice on an ssh entry that could invalidate the
envelope, so no drop condition is needed.
Fixes#103795
Review follow-ups on the #96187 cherry-pick: skip on win32 like the
sibling tests that shell out; reuse the file's `exec` helper; one temp
root so a failing second mkdtemp cannot leak the shim dir; quote the
shim's redirect target; guard the payload-prefix sentinel so a renamed
loop marker can never make the test execute the real spawn payload;
drop the lockMetadata fields the assertions never read. Move the
mutexPath contract into withRemoteUpdateMutex's doc comment instead of
a third inline restatement.
expandRemotePath() returns an already-quoted shell fragment
("$HOME"'/path'), but three call sites wrapped its output in shq()
again: the withRemoteUpdateMutex python argv, the reservation/lock/
owner_file assignments in buildSpawnCommand, and the identity values in
buildOwnedStaleTerminationCommand.
The remote shell strips only one quoting layer, so python received a
mutex path with literal quote characters in it (creating a directory
literally named ' in $HOME), and the payload's mkdir "$reservation"
loop spun on a path that can never exist. Every Desktop SSH backend
spawn hung until the connect timeout, retried, and left an orphaned
flock queue behind; stale-owner cleanup always printed REFUSED for the
same reason.
The regression test parses the composed command with a real sh — the
same parse the remote login shell performs — and requires the mutex
path and the payload's reservation paths to come out fully expanded.
spawnPoolBackend() is not on every dial path: a primary route (startHermes),
a registry remote scope (connectRegistryBackend), a reused primary SSH
backend, or a guard rejection all settle the claim without requesting a
slot, so a foreground mark set for that dial stayed in pendingForegroundSpawns
and would have upgraded the next background hydration spawn of the same key.
applySpawnPriority() now returns the cleanup; both IPC handlers run it in a
finally around the claim. The mark is also taken right before the slot
request instead of at function entry, so the remote branch never consumes it.
Follow-ups to the #102496 salvage in the Electron main process:
- pendingForegroundSpawns leaked: promoteInFlightLocalSpawn marked the key
even when the pool entry already existed (the common click path), and
nothing consumed it. After that backend was reaped, the next dial for the
key - normally 10 s roster hydration - spawned as foreground and sat in the
reserved slot. Mark only when no entry exists yet; spawnPoolBackend consumes
the mark before any early return (remote route included) so it never
outlives the dial.
- The "waiting for a free local slot" log fired for a foreground request that
was granted the reserved slot immediately (condition was queuedCount > 0
after request()). The request now reports `queued`; log only then.
- One promotePoolEntry() and one logPoolSpawnFailure() replace three copies of
the promote snippet and two copies of the background/foreground log branch;
spawnPoolBackend reads entry.spawnPriority instead of a second opts channel;
isBackgroundSlotWaitTimeout is an instanceof check (same process as the
class, no duck typing).
LocalBackendSpawnCoordinator is FIFO with maxBackends=3, so launch
hydration queues ~27 ensureBackend calls and a user click times out
waiting for a slot. Reserve a foreground slot, drain foreground first,
and fail background slot-wait quietly.
Fixes#102281.
The terminal pane's registry-scoped lookup keys sshConnections with
backendScopeKey(connectionId, profile), but the pool's single writer
publishes every tunnel under its per-profile bootstrap key with the
registry connection stamped on the entry. The lookup can never hit, so
a v2 registry SSH terminal pane stays 'pending' forever (#97345).
Fall back to the entry stamped with the window's connection id - the
same identity match managedSshScopeRole applies to pool entries - and
return the writer's actual scope so downstream getSshConnectionState
and teardown keep working.
resolveDesktopRemoteRoute tags v1 SSH with a registry connectionId when
the host matches. The terminal then looked up sshConnections with
backendScopeKey(connectionId, profile), but bootstrap stored the
tunnel under sshScopeKey(profile or null). The pane stayed pending.
Ignore the identity tag for v1 pool lookup.
Follow-ups on the cherry-picked handler:
- a throwing observer can no longer change the decision; the handler returns
an explicit deny regardless of logging failures
- the denied-URL log carries origin only, so query tokens / signed URLs from
attacker-controlled content never reach the persisted desktop log
- the hidden link-title window (loads arbitrary user-linked pages on render,
had no window-open handler at all) now denies too
- tests trimmed to two invariants (proven red against the pre-fix shape)
setWindowOpenHandler opened details.url as a side effect before denying.
Per GHSA-9f4c-93c8-jc8g (CVE-2026-70608, High 7.2), a sandboxed iframe
with no allow-popups and no user gesture can reach this handler via the
OpenURL path -- and the desktop renders untrusted artifact HTML in
<iframe sandbox="allow-scripts">. A malicious artifact could therefore
force the OS browser to an attacker URL with zero interaction. Electron
ships no fixed 40.x release (fix is 41.10.3+/42.0.1), so we close it at
the seam, version-independently.
- electron/window-open-policy.ts: pure decideWindowOpen (always deny) +
createWindowOpenHandler(onDenied) that denies and never opens a URL;
the hook is logging-only.
- main.ts: wireCommonWindowHandlers uses it (covers primary + all
secondary/quick windows); the deny is logged, no side-effect open.
- Trusted external links are unaffected: they already route through the
audited hermes:openExternal IPC channel (openExternalUrl, http/https/
mailto allowlist). Converted the one remaining bare window.open on the
Electron path (env-var docs menu) to openExternalLink; other
window.open sites are bridge-absent web fallbacks.
- tests-js/window-open-policy.test.ts: 4 tests pinning always-deny, the
logging-only hook, and that a throwing hook never degrades to allow.
hermes_cli/plugin_compat.py is now the single source of truth for the compat window:
COMPAT_REMOVAL_DATE = 2026-09-14; scan_plugin() statically finds `from F import n`, `import F` + `F.n`,
alias forms and string targets against compat_manifest.json; compat_report() aggregates over the user's
ENABLED external (non-bundled) plugins; disable_reason() decides the loader's skip.
Surfaces (all read from that one report):
* CLI: yellow block under the banner naming plugins + date + `hermes plugins compat` (red + DISABLED after)
* `hermes plugins compat [--json] [path]`: file:line, old -> new per hit; exit 1 while anything remains;
`path` lets a plugin author scan their own checkout
* `hermes doctor`: "Plugin import paths (removed Sep 14, 2026)" section next to the xAI retirement check
* `hermes update`: post-update notice alongside the FTS/curator notices
* Desktop: compat_report() writes HERMES_HOME/.plugin-compat-report.json (deleted when clean); Electron
shows ONE warning dialog per distinct report after the backend is up and persists the dismissal in
userData/plugin-compat-dismissed.json. A new affected plugin, or the date passing, is a new report.
From the date, PluginManager skips a hitting external plugin before importing it, with the reason in
LoadedPlugin.error ("uses N import path(s) removed on 2026-09-14; run `hermes plugins compat` ...") — the
same path a plugin with a broken register() takes, so nothing else is affected. Escape hatch:
plugins.allow_deprecated_imports: true (config_defaults), which only helps until the compat commit is
actually reverted.
Docs: COMPAT_MANIFEST.md (removal date, what-happens table, author instructions), plugin dev guide section.
Tests: tests/test_plugin_compat_notice.py (scanner forms, report scope, date gate + escape hatch, summary
text, report file lifecycle, loader skip via a real PluginManager), electron/plugin-compat-notice.test.ts
(show once, re-show on a different set or on the date passing, malformed file ignored).
Live A/B on this box with a demo plugin on old paths: before the date it loads and the banner/doctor/report
name it; with today=2026-09-14 it is skipped with the reason and the banner turns red; with the escape
hatch it loads again.
readPersistedPoolLimits() runs at module evaluation and logs through
rememberLog() on every branch, but hermesLog / desktopLogBuffer /
desktopLogFlushTimer / desktopLogFlushPromise were declared ~110 lines
later. esbuild lowers const/let to var, so the packaged desktop died on
every launch with "Cannot read properties of undefined (reading 'push')"
(#101941, #101960). Moving the four declarations above the read fixes the
crash and keeps the early [pool-limits] line in desktop.log.
Salvaged from #101945 (test dropped: Desktop E2E lane is disabled in CI).
Composition of #92581 on #100985: the hard cap is a constructor constant,
so raising the pool max in Settings would have left new spawns queued
behind the launch-time value. Add setLimit(); slot hand-off now goes
through a single #drain that respects the current cap, which also fixes
the original release path handing a slot to the next waiter even when the
cap had just been lowered (test: lowering never revokes granted slots; new
requests queue until under cap). main.ts constructs from
poolLimits.maxBackends and pushes changes from setPoolLimits(); pinned by
a wiring test.
Hover-intent prewarm sweeps across the Bots rail spawned past the pool cap,
LRU-evicting the backend the user was about to click — an evict/respawn
cascade that made profile switching progressively slower (#91545).
- prewarmProfileBackend skips speculative spawns once every pool slot holds
an open socket; the real click still spawns on demand.
- Pool max/idle become a device preference (Settings -> Advanced), persisted
atomically in userData (pool-limits.json) and applied live over IPC; the
HERMES_DESKTOP_POOL_* env vars remain the initial fallback. Defaults are
unchanged (3 backends / 10 min idle).
Squash of the 3-commit PR #92581 branch (a00dc088c5..783899d12f) applied
via diff onto the spawn-coordinator salvage; import + constant-block
conflicts resolved so the coordinator is constructed from, and follows,
the live preference (setLimit added in the next commit).
Follow-up to the spawn coordinator: the queued ticket waited up to
POOL_IDLE_MS (10 min) for a free local slot, but the renderer gives up on
a backend boot after 45 s. A user clicking a 4th profile with 3 fresh
backends open would see the generic "backend didn't come up" error while
the ticket kept the pool key hostage, so every later click joined the same
stale wait. Cap the wait at 30 s, log the slot pressure when it happens, and
pin the relationship to BACKEND_BOOT_WAIT_TIMEOUT_MS with a wiring test
(fails when the timeout is reverted). Also eslint --fix on the salvaged
files (import order was a lint error).
Desktop could spawn a local hermes serve per profile with no hard cap on
starting+running children: LRU eviction spares keepalive-fresh entries, so
a roster refresh across many profiles became a process wave (40+ backends,
load 30-50 reported).
LocalBackendSpawnCoordinator: at most POOL_MAX_BACKENDS local backends may
be starting or running. Remote descriptors never take a slot. Queue tickets
are per request; a slot is released only after process exit is proven
(exitCode/signalCode). A rejected wait keeps the slot occupied. Pool entries
re-assert ownership at each await so an evicted entry cannot spawn a zombie.
Squash of PR #100985 (6017abbbc4 + merge), applied via diff onto current
main. Original commits were authored as 'Motor (Hermes AI) <ceo@xtremagency.com>';
attributed here to the PR author's GitHub identity.
The ownership file accumulates one record per profile per launch, and each
record can cost up to two identity probes (parent + backend) plus a stop.
On Windows those shell out to PowerShell, whose 5.1 cold starts are slow.
Without a bound, a large roster could stall boot for minutes while the
renderer's 45s backend-boot budget expires and the user stares at the
connecting screen.
- Add REAP_PROBE_TIMEOUT_MS (5s) for the orphan-reap path; the claim path
keeps the full 30s headroom for a freshly spawned backend's marker.
- Add reapDeadlineMs (5s default) as an overall budget for one reap sweep;
when exhausted, unprocessed records are preserved for the next launch.
- stopOwnedBackend now throws when the identity probe fails (not confirmed
gone) so the record is preserved instead of leaking the backend.
Verified: two cold starts complete in ~29s (was 5+ min); renderer connects
immediately after backend ready.
`openSessionInNewWindow` → IPC `hermes:window:openSession` →
`buildSessionWindowUrl` emitted no `profile`, so a secondary window (⇧⌘-click
pop-out, subagent watch) was a full renderer that adopted the PRIMARY
backend's profile and resolved the session id against the wrong store —
blank/wrong session for any non-primary profile (#82768, #61286).
The owning profile now rides the URL as `&profile=`, exactly the carry the
HUD already does (buildHudWindowUrl / windowProfileOverride in
use-gateway-boot); the renderer picks it with the same ladder openHud uses:
the session's stamped owner wins, an unstamped/uncached id (a brand-new
subagent child) inherits the profile the user is looking at.
Diagnosis credit: @DomGrieco (#82794).
Co-authored-by: DomGrieco <6556434+DomGrieco@users.noreply.github.com>
resolveRegistryLocalRoute collapsed globalRemote and profileRemoteOverride
into one forced-local branch. The two cases are different:
- globalRemote: forcing "This device" to spawn genuinely-local children is
the intended migration behavior — unchanged.
- profileRemoteOverride: the per-profile SSH/remote override is an explicit,
authoritative routing decision for that profile. Forcing local made the
roster enumerate the profile via its override but open the thread in a
forced-local child, which dies with 'Profile "x" no longer exists' when
the profile only exists on the remote — reproduced on a macOS Desktop in
global SSH mode where mythony-agent/q-agent exist only on the NAS.
The registry 'local' entry now delegates to the legacy profile route when a
per-profile override is present, so the override stays authoritative.
Tests: the override case now pins delegation, and a new witness pins that
globalRemote alone still forces local; both contracts are asserted together.
88 connection-registry + 91 remote-lifecycle + 73 routing tests pass;
tsc --build clean.
When the bundle was swapped under a running process, the About banner sent the
user to the installer — a download and a reinstall for a state that a plain
restart repairs, and the reason reinstalling never helped these reports.
Report bundleSwapPending on hermes:version and give that case its own copy and
a "Restart Hermes" button. It gets its own headline too: reusing "App build out
of date" over a body that says the app is already installed repeats the
contradiction with the Updates card that the banner is supposed to resolve. The
installer link stays for the genuinely-stale-bundle case.
Packaged builds only — a dev `--build-only` rewrites the stamp under a running
`npm start`, and that is a rebuild the developer asked for, not a torn install.
Co-authored-by: tk-pkm111 <133480534+tk-pkm111@users.noreply.github.com>
A user who reopens Hermes while an update is running lands on the boot gate,
which is what it is for. But the updater swaps the packaged bundle on disk
after `hermes update` exits, and its `open` leg only focuses this already-
running process, so nothing ever loads the new build. The parked instance then
passes the gate and boots the new runtime under the old renderer — the "App
build out of date" banner immediately after a fully successful update, over an
Updates card that says "You're on the latest version" and so offers no remedy.
Compare the install stamp this process loaded at boot with the one on disk when
the gate clears. On positive proof of a swap — different commit, or a different
builtAt at the same commit — relaunch instead of starting a backend. Detection
fails quiet like bundle-skew, so a swap that never happened (the Windows
locked-binary case) is unchanged. A one-shot argv flag makes a relaunch loop
impossible and a 15s failsafe falls back to the old behavior.
Co-authored-by: tk-pkm111 <133480534+tk-pkm111@users.noreply.github.com>
Co-authored-by: aeonsong <aeonsong@users.noreply.github.com>
detectBundleSkew() trusted `git rev-list --count <stamp>..HEAD -- apps/desktop`
outright, which claims skew in two states where the install is not torn.
Ancestry: `A..HEAD` only measures how far HEAD is ahead of A when A is an
ancestor of HEAD. A ZIP-fallback update rewrites the tree onto a synthetic
root, so the stamp still resolves but is unreachable; the range degenerates to
HEAD's own history and reports a permanent >= 1 while apps/desktop is
byte-identical. Ask `merge-base --is-ancestor` first and go quiet unless it
answers yes.
Scope: the pathspec counted every file under apps/desktop/, so a docs- or
e2e-only commit produced a banner promising missing UI features that do not
exist. Count only the paths that reach the shipped app.
Co-authored-by: jackulau <jackulau@users.noreply.github.com>
Co-authored-by: kokhlo <kokhlo@users.noreply.github.com>
Run models locally as a first-class provider. The CLI grows a managed
llama.cpp runtime (engine install, model download, server supervision);
the desktop app grows the full setup and management story on top of it.
GUI surfaces ship behind the desktop --local launch flag (hermes desktop
--local, or the flag on the packaged app); backend routes and the CLI
are always live.
Runtime (hermes_cli/local_runtime/):
- curated GGUF catalog with per-machine variant selection: hardware
probe (VRAM/RAM/UMA), fit planning with spill accounting, quant choice
by context window
- derived recommendation: quality-ranked picks gated by a predicted
decode-speed floor, bandwidth-aware on unified memory; the decision
table is pinned as a test (pick AND reason per memory class), and the
Recommended badge explains its pick in a tooltip fed by the resolver's
actual branch
- engine install + model download with resumable split parts, cumulative
plan-level progress, and staged-model integrity (a split GGUF counts
only when every part is present)
- server supervision: spawn/adopt/stop, router mode with per-model load
progress relayed over SSE, abandoned-request cleanup
Desktop:
- Settings -> Providers -> Local models: one-click quickstart (install
engine, download the recommended model, boot) plus per-model download/
activate/eject, fit-ranked catalog with context pills
- model pickers (composer dropdown + Cmd+K) show staged local models,
in-flight downloads as live progress rows, and load-into-memory bars
- local-setup campaign tip for eligible hardware; System resources
statusbar widget (GPU/VRAM/RAM); in-chat load progress during sends
- friendly dead-server errors, and failed agent builds retry on the next
send instead of wedging the session
Co-developed with NVIDIA field feedback on RTX 5090 and DGX Spark.
_migrated:true files were skipped on later boots, so #100576 installs
stayed stuck on the named profile. Re-evaluate those files only; leave
user-selected pins (no _migrated) alone. If default now wins, write
{profile:null} instead of pinning default.
First-boot migrateActiveProfileIfMissing only listed ~/.hermes/profiles/*
and scored profiles/<name>/state.db. Default's real DB is ~/.hermes/state.db,
so a tiny named profile could be pinned after an update.
Always candidate default, score/pid-check it at HERMES_HOME, and do not
write active-profile.json when the winner is default.
Fixes#100576
- curly: brace all single-line if statements in profile-migration.ts and
profile-migration.test.ts (17 errors in CI check:lint)
- perfectionist/sort-imports: node:fs builtin import before vitest external
- padding-line-between-statements: blank lines after block statements
- prettier: normalize formatting (fmt script style) in the three touched files
All 967 electron project tests still pass.
Polish from antigravity review of the rebase resolution (GPT-OSS):
the previous comment said "BEFORE the first primaryProfileKey() /
primaryBackendIsRemote() read" but those two calls live at different
points — primaryBackendIsRemote() is the very next line, primaryProfileKey()
is inside the connection IIFE. Be explicit about which is where so a
future reader who moves one of them knows what to preserve.
Addresses teknium1's review (#64195) finding #2: the multi-rung resolver
needs Electron tests covering precedence, stale-PID rejection, fallback
behavior, and the remote boot path. The pure decision helpers are now
covered by 29 unit tests in `profile-migration.test.ts` (vitest electron
project).
Coverage:
- precedence: legacy > single-running-gateway > state.db heuristic
- stale-PID rejection: recycled PIDs not owned by hermes are dropped
- malformed pid files: JSON parse errors, non-integer PIDs, zero/negative
- scoring edge cases: ancient files (recency floored at 0.1), tiny files
(size floored at MIN_SIZE), larger DB beats smaller at similar recency
- single-profile fallback: best === 'default' suppresses the write
- no-op cases: preference file already exists, missing profiles root
The remote boot path is verified by code review of the call-site move
(commit preceding this one) — `migrateActiveProfileIfMissing()` now runs
before `primaryProfileKey()` is first read in `startHermes()`.
The pure decision logic that the orchestrator relies on is covered end-
to-end below; this matches the repo's testable-helper pattern (see
`profile-delete-routing.test.ts`).
Addresses teknium1's review (#64195) finding #1: the previous PR placed
the migration inside the connection IIFE, AFTER
`resolveRemoteBackend(primaryProfileKey())`. When the preference file
was missing, `primaryProfileKey()` resolved to 'default' and the remote
branch returned immediately without ever reaching the migration. Remote-
mode users got no migration at all.
Move the call site to the top of `startHermes()`, before the connection
IIFE that reads `primaryProfileKey()`. Both remote and local branches now
flow through this path before any profile-dependent resolution, so the
migration runs on first boot regardless of mode.
The inlined implementation is replaced with a thin wrapper that builds a
`MigrationDeps` bag and delegates to `migrateActiveProfileIfMissing` from
`profile-migration.ts`. No production behavior change beyond the call-
site move.
Tests added in a separate commit.
Addresses teknium1's review (#64195) — the migration decision logic should
be unit-testable without Electron. Pull the ladder (legacy sticky file,
running-gateway scan, state.db heuristic) into pure helpers in a new
`profile-migration.ts` module, following the dep-injection pattern
already established by `profile-delete-routing.ts`.
The helpers take an injected `MigrationDeps` bag so tests can exercise
precedence, stale-PID rejection, fallback behavior, and the single-profile
case without touching `/proc`, `ps`, or the host filesystem. The default
profile is explicitly rejected from the legacy rung because the regex
matches `default` and accepting it would suppress the heuristic that is
the whole point of the migration.
The wrapper in main.ts is unchanged in behavior — the same `MigrationDeps`
fields get filled in from `fs`/`path` and `isHermesProcess`. The atomic
write + parent-dir-create that the wrapper performs matches
`writeActiveDesktopProfile`'s semantics so the migration produces a file
indistinguishable from a user-driven profile switch.
No production behavior change; pure code organization.
Tests added in a separate commit.