`activate` doubled as the thing scripts sourced non-interactively, so it
carried `--` argument guards and every caller had to tolerate its prompt and
`hermes` function. Composition (setup sync, bootstrap Python, pm env) now
lives in scripts/_activation.sh, and there are two consumers:
- `activate`: interactive, reversible (`deactivate`).
- new `scripts/run-in-hermes-env CMD...`: applies the env to one process and
execs into it. Replaces `scripts/_hermes-python`; every Python script's
shebang and `scripts/run_tests.sh` (which re-executes itself under it,
however it was launched) use it.
`pm.environments --format sh` emits the env as shell exports, so the inline
JSON-to-export Python is gone from `activate`. An install that did not build
the test environment now reads as stale for every consumer.
Tests are split along the same seams: activation support fixtures, bash/ps1
activation, env-script quoting, and the runner's staleness.
With shared metrics on, every provider stream runs through Relay's managed
pipeline. Relay pulls the next provider chunk before it delivers the current
one, and the provider read blocked the stream's private loop, so each chunk
was withheld until the next arrived. On Desktop the reply looked stopped while
the timer kept going and queued follow-ups waited on a turn that was still
running; a steer aborted text the user never saw, stacking user bubbles with
no output between them.
The off-loop read from the previous commit fixes it. Correct its comment to
the real cause, replace the two-loop starvation test (production gives every
stream its own loop, and it bounded wall-clock at 150ms) with a single-stream
contract that deadlocks on the old code, and add a core e2e spec that steers
and queues against a paused provider with Relay managed execution on.
73c17151f6 (Sep 29) persisted maintenance.auto=false, gc.auto=0 and
fetch.writeCommitGraph=false in treeless install checkouts. 6880ffc1af
stopped writing gc.auto=0 but never removed it, and `gc --auto` with
gc.auto=0 is a no-op (git skips every heuristic, gc.autoPackLimit included),
so the update's pack fold never ran on those checkouts: 203 -> 203 packs.
The previous commit's fingerprint required gc.writeCommitGraph=false, which
only the Oct 2 cut wrote, so the Sep 29 cohort also kept maintenance.auto=false.
Both cuts wrote maintenance.auto=false together with
fetch.writeCommitGraph=false and neither wrote
maintenance.commit-graph.enabled; that pair is now the fingerprint, and
gc.auto=0 is removed under it as well. An operator's own lone
maintenance.auto=false still never matches.
Real clear_git_debris() on a treeless clone with 203 lazy-fetch packs, git
2.53.0 and 2.55.0 alike:
Sep 29 keys: main 203 -> 203, #131741 203 -> 203, this 203 -> 1 (both keys gone)
Oct 2 keys: 203 -> 1, maintenance.auto gone
operator's own maintenance.auto=false: 203 -> 1, setting kept
_restore_auto_maintenance unset any local maintenance.auto=false, so an
operator's own setting on a partial-clone checkout was erased on every
update (nothing records who wrote it). Remove it only with the first cut's
fingerprint: maintenance.auto, gc.writeCommitGraph and fetch.writeCommitGraph
all false and maintenance.commit-graph.enabled not yet set. The first pass
of disable_tree0_auto_maintenance writes that key, so the migration runs once
and an operator's setting is left alone before and after.
Review point from @Godhet on #131718.
#131679 set maintenance.auto=false in the treeless install checkout to stop the
commit-graph lazy-fetch loop (#127711). That also switched off the post-fetch
`gc --auto` that git <= 2.53 runs after every lazy fetch, so packs piled up
between updates and only folded on a manual `hermes update` / `--check`
(nothing runs `--check` automatically).
The loop needs a commit-graph write, and gc.writeCommitGraph=false +
fetch.writeCommitGraph=false already stop gc and fetch from writing one.
maintenance.commit-graph.enabled=false closes the last writer (a commit-graph
maintenance task, e.g. from a user's global maintenance.strategy) while
leaving automatic maintenance, and its gc fold, on.
Checkouts that already got maintenance.auto=false from the first cut are
converged: the next update/--check unsets exactly that value.
Measured on a real tree:0 clone with a Bloom commit-graph, 100 lazy fetches
after one updater run:
git 2.53.0: main 101 packs, this change 1 pack, 0 commit-graph writes
git 2.55.0: 101 / 101 (stock 2.55 maintenance does not fold), 0 writes
On macOS, `hermes update` and `hermes desktop` reinstall any Hermes.app
listed in desktop-installed-apps.json that has gone missing, and the GUI
uninstall is what deletes that record. gui_is_installed() ignored the
record, so after the app was removed by hand `hermes uninstall --gui`
reported that no GUI was installed and returned without deleting it, and
the next update put the app back.
Count the install record in gui_is_installed() so the uninstall previews
and removes it. No new removal targets: uninstall_gui() already deletes
the record. The retention tests now also run with the record as the only
remaining GUI artifact.
Refs: #128974
Salvaged-from: #131147
The daily package is cut by the first export that runs, and the export runs on
the `hermes-shared-metrics-flush` worker. The test asserted the exact metric set
of a package cut after the task closed but before the session-summary rows
(`hermes.context_peak.count`, `hermes.session.count`) were emitted, and read the
outbox without waiting for the worker. Which side of the session close the
worker landed on was scheduler luck.
Delaying the worker (sleep in `_flush_and_export`): 0.01s and up cut the package
after the session close and the assertion failed on the extra metrics; with no
wait at all the outbox could still be empty.
Join the worker the task close scheduled before the session closes. The package
is then always the task's, and the read no longer races it.
Session close schedules `subscribers.flush()` on the `hermes-shared-metrics-flush`
worker (806acb68ec). With the real Relay binding, the flush is what drains the
native subscriber queue, so a test that reads the SQLite store right after
`finalize_session` can see zero counters. The sibling real-binding tests already
call `_join_export_workers()` first; this one did not.
Reproduced by sleeping 0.3s in `SharedMetricsSubscriber.__call__` (native
delivery lag): the test failed with `not enough values to unpack (expected 1,
got 0)` before the join and passes with it.
Five tests loaded desktop-bundled-release.yml and asserted on its shape or
evaluated its `if:` expressions with a mini evaluator. They restate the YAML,
so they fail on correct edits and pass on subtly wrong ones. The tests that
execute the workflow's shell steps against a fake R2 stay.
stable-release.yml runs `jobs: win32-bundle` as its own call after the
per-arch candidate calls. In that call validate selects only win32-bundle, so
assemble-win32-bundle's gate (win32-x64/win32-arm64 selected AND their build
jobs succeeded) was always false and the job skipped, which failed the stable
bundle phase with `assemble-win32-bundle=skipped`.
A build selected in the same call must still succeed; an unselected one is
fetched from the R2 handoff, as the assemble steps already do.
The fold was killed after 120 s on the assumption it takes "well under a second". That holds
for ~100 small packs, not for the checkouts it exists for (#129712: 2,475 packs / 39 GiB): those
need a full repack, and a killed fold restarted from scratch on every update without a word,
so the worst-hit installs never converged.
- Bound raised to 20 minutes (measured: 750 packs / 530 MB of hermes-agent history fold in
5.8 s at 7.4 cores; a 39 GiB pack dir on a slow disk is a different order of magnitude).
- consolidate_lazy_fetch_packs returns None on timeout; the shared update_cmd_check reporter
(used by both `hermes update` and `hermes update --check`) prints the hand-fold command.
- on_fold_start announces a fold that will actually run (pack count past gc.autoPackLimit), so
a multi-minute first fold is not a silent hang.
- Test fixture disables auto-maintenance: each lazy fetch's detached gc otherwise raced the
test and folded the packs itself.
Replaces the salvaged tests, which mocked subprocess and the gc command line,
with real-git invariants on a blobless clone:
- lazy-fetch packs fold, keep their .promisor markers, and the history still
reads under GIT_NO_LAZY_FETCH, with the maintenance keys applied (gc.auto=0
makes this fail);
- the fold adds no pack for commits a Bloom commit-graph has not seen (11
packs instead of 1 without the guards);
- `hermes update --check`'s debris cleanup is where the fold happens;
- a full clone keeps git's stock maintenance.
The two salvaged commits above disagree: #127721 persists gc.auto=0, which
turns the fold's `gc --auto` into a no-op (106 packs stay 106 on git 2.50.1,
2.53.0 and 2.55.0), and the fold itself is exposed to the loop #127721
guards against. Probed on all three gits against a blobless clone whose
commit-graph carries changed-path (Bloom) data:
- gc, `gc --auto`, `maintenance run --task=commit-graph` and
fetch.writeCommitGraph each lazy-fetch the trees of every commit the graph
has not seen (30 unseen commits: 30 fetches, 30 packs), and every lazy
fetch spawns `git maintenance` again. gc.writeCommitGraph=false stops it
(gc: 31 packs -> 1).
- Without a Bloom graph none of that happens, so the keys are a guard, not a
fix for every partial clone.
Changes:
- keys: maintenance.auto, gc.writeCommitGraph, fetch.writeCommitGraph; gc.auto
stays default. Idempotent (no rewrite when already set), `--local`, hidden
console on Windows. install.sh and install.ps1 set the same three keys.
- consolidate_lazy_fetch_packs applies the keys before the fold, so installs
that predate the installer change converge on their next update check.
- the fold runs under bounded_probe_run: subprocess.run(timeout=) kills only
`git gc` and leaves `pack-objects` running (reproduced), and with
GIT_NO_LAZY_FETCH.
- dropped the post-gc mark_unmarked_packs_promisor: gc keeps the .promisor
marker on 2.50.1, 2.53.0 and 2.55.0.
- docs: updating.md troubleshooting entry.
Every on-demand fetch on the promisor remote of a tree:0 installer
checkout writes its own small packfile, and no update-path step ever
consolidates them, so .git grows without bound (2,475 packs / 39 GiB
observed for a ~1 GiB repo, #129712).
Ride git's own threshold: run `git gc --auto` from the debris-cleanup
step both `hermes update --check` and `hermes update` already run.
Below gc.autoPackLimit (default 50) small packs it exits in
milliseconds; past it, the lazy-fetch packs fold into one. The folded
pack is re-marked .promisor so git 2.53+ fetches keep working (#124272),
and the gc does not write a commit-graph, which git <=2.50 misreads on
the next lazy fetch.
`hermes uninstall --gui` deleted hermes-agent/node_modules on the premise
that the workspace root only carries Electron. The root package.json is
an npm workspace (apps/*, ui-tui, ui-tui/packages/*, web, tests-js), so
the root node_modules holds the hoisted dependencies of the TUI and the
dashboard too, while Electron itself resolves from
apps/desktop/node_modules, which is still removed.
Stop treating the workspace root as a GUI artifact, list it under "Kept
intact" in the removal plan, correct the docstring and the desktop guide,
and add a regression test that a real `--gui --yes` removal keeps agent
data and the shared dependencies while removing every GUI artifact.
Refs: #128974
Salvaged-from: #131147
A dry run never prompts, but plain `hermes uninstall --dry-run` still went
through the interactive-terminal gate and was refused when stdin was not a
TTY. Exempt --dry-run from that gate, matching --gui and --data.
The GUI removal plan also omitted desktop-installed-apps.json, which
uninstall_gui() deletes. List it under "Will remove" so the preview matches
what a real run removes.
Adds a CLI-level regression test covering `--gui --dry-run` with and
without --yes, and plain `uninstall --dry-run`, asserting nothing is
prompted or removed.
Refs: #128974
Salvaged-from: #131147
`hermes uninstall --gui --dry-run` really deleted the GUI artifacts
(including the workspace node_modules) instead of printing a plan:
run_gui_uninstall() had no dry-run branch, so the confirmation prompt
still ran and answering "yes" executed uninstall_gui() for real. The
--data and default uninstall modes already honored the flag.
- run_gui_uninstall(): after printing the removal plan, print the dry-run
notice and return before the confirmation prompt (mirrors
run_data_uninstall), so both the interactive and --yes paths are covered
- cmd_uninstall(): exempt --dry-run from the --gui TTY gate, like --data
already does — a dry run never prompts, so it needs no TTY
- make _hermes_path_markers' docstring a raw string: its git\cmd / git\bin
examples emitted a SyntaxWarning on every uninstall run (reported in the
issue's output)
Refs: #128974
Salvaged-from: #128981
Co-authored-by: Yuan Li <dskwelmcy@163.com>
The update gate read the baked stamp by walking up from the renderer dist and
re-parsing it by hand; resolve the resources dir once (_renderer_bundle_dir now
builds on it) and read the stamp through steward.read_install_stamp. The test
builds its fixture from the same resolver instead of copying the layout.
build_update_products rebuilt, repackaged and reinstalled the desktop app on
every completion, including 'Already up to date', retried tails and takeover
re-entries, and could stop a running app to free the output. Reuse it when the
commit the packaged app ships (its baked install-stamp.json) is HEAD and its
freshness receipt is current. Freshness alone is not enough: the baked stamp is
an input only the build rewrites, so after a pull that touched no other desktop
input the receipt reads current while the app names the previous commit.
Keep newly observed sleep credit until the following poll because the supplied idle duration can predate that sleep. Extend both contributor regression tests across the sampling boundary and qualify the platform guarantee.
Add AwakeIdleMeter, which reads the wall clock and time.monotonic() on
each watchdog check. monotonic pauses during sleep on macOS
(mach_absolute_time) and Linux (CLOCK_MONOTONIC), so the gap between the
two clocks is time spent asleep, and that sleep is not charged as idle
time until the agent records new activity. The cron inactivity watchdog
and the turn liveness watchdog both use it, because on main both guard a
cron run with a 600 s wall-clock bound. A job that is really idle for
600 s awake is still killed.
Fixes#131523
(cherry picked from commit 07d9ddc24736e243d72327ff90a24f95e86119d8)
`release.py abandon` deleted the draft and wrote the marker but left the
attempt's Stable Release run going. A live run keeps building, and with
autopublish on it can publish the draft that abandon just cleared.
abandon now lists the claim ref's runs of stable-release.yml and runs
`gh run cancel <id> --force` on every one that has not completed. This
happens before the draft is deleted and before the marker is written: the
marker removes the attempt from the outstanding set, so a cancel failure
after it could never be retried. A published release is still refused
before anything is cancelled. The `delete` callable is renamed `execute`
since it now runs cancels too.
The plugin deep link (hermes://plugin/install?catalog=) and the skill
deep link are separate features: one per kind. Reverting #119381's card
catalog should not leave skills without a link, so this restores the
skill route on its own:
- Desktop: skill-deeplink-install store, the skill route in
deeplink-routes, its use-desktop-integrations wiring, and the
confirm-store onConfirm/details/busy/done phases its dialog uses
(restored from main with their tests). Its nine strings move from the
deleted catalog.* namespace to skillDeepLink.* (en + the six locales
that translated them).
- Website: Skills Hub "Install in Hermes" button, plus the
source-qualified installIdentifier from extract-skills.py so the link,
the embedded picker and the CLI command all name the exact skill
(apps/shared/src/catalog-install.ts keeps only the skill helpers).
- Docs: skills.md "Install from the website" section, desktop.md pointer.
The card catalog UI stays reverted.
Reverts all seven commits of #119381 (59c54c78..476ff4dd), which
re-landed the native card-based Skills/Plugins catalog browser after it
was reverted twice (#113037 reverting #112669, #115787 reverting
#113128). Capabilities -> Skills and Plugins return to the list/detail
tabs with the embedded catalog picker they had before #119381.
Removed with it:
- Desktop catalog/ module (browser, cards, detail dialog, filters,
reels/masonry, install switch) and skill deep-link install
(hermes://skill/install); skills/embedded-hub-picker.tsx comes back.
- apps/shared catalog-browse.ts and catalog-install.ts.
- The `catalog` i18n namespace in every locale (+ regenerated
locales/_keys.desktop.json); the two i18n pack tests and the keys
test now exercise connectorsPage.searchPlaceholder instead.
- Website /skills "Open in Hermes Desktop" skill links and the
extract-skills install identifiers; docs return to the pre-#119381 text.
- Later fixes that only patched the reverted catalog code go with it:
5832a6eb93 + 7aba2cbbe9 (catalog download deadline),
9e63cdc4b8 (catalog lookalike rows), c756986db2 (catalog counts),
the catalog half of c13232e90c / f265273347.
Kept on purpose (later work that depends on code #119381 introduced):
- DialogContent `chrome` / `overlayClassName` / nested-dialog layering
(the image lightbox uses them).
- vitest.setup.ts / src/test/jsdom.ts stubs (other suites rely on them).
- Kanban toolset wiring on the Plugins Desktop switch (cd30fdadf6,
7ca2f5665f) and its tests, the `hermes_not_connected` server tone,
the SearchField flex-1 fix, skill provenance helpers. SkillDetail shows
a provenance pill only for agent/hub skills, since the pre-#119381
labels have no `external` entry.
Replace the bare-instance, wall-clock-bound unit test with two invariants run
through the real lifecycle hooks: a task end returns while the process-wide
flush barrier is parked (later task ends coalesce into one extra pass), and the
background worker runs under the finishing turn's profile. The existing runtime
tests that read the store right after a hook now join the worker first, and the
daily-package test joins after its first session so the package forms where the
test expects it.
Follow-up to the off-turn flush worker, found while running the runtime tests:
- The worker was a bare threading.Thread, so the export it runs lost the
contextvars of the turn that scheduled it. _send_exported_packages reads the
profile's send/consent config through them, so under a multi-profile serve the
export resolved the launch profile's config instead of the finishing turn's.
Start it through spawn_context_thread.
- A trigger arriving after the worker saw "nothing pending" but before the
thread finished dying found is_alive() true, set the pending flag and started
no worker, so that flush waited for the next trigger. The worker now clears
its slot under the lock schedulers check, and the slot is set only after
start() succeeds.
The TUI (and the dashboard that embeds it) answered vault.save_login and
vault.code with -32601, so a browser sign-in could never save a login or
take a 2FA code there.
Builds on liuhao1024's save-login card: both vault cards now sit beside
the unlock prompt in maskedPrompt.tsx on the shared MaskedPrompt (the
identifier and the code are shown as typed, the password is masked), all
copy is localized, and one SENSITIVE_PROMPTS list drives Esc/Ctrl+C
decline, request.cancel teardown, the tab-title marker and the pet, so a
new credential prompt can't be missed by one of those gates (Esc on the
save-login card previously did nothing).
Co-authored-by: fangliquan <fangliquan@qq.com>
Co-authored-by: Uzair Chhapra <uzairchhapra@gmail.com>
Rebased onto the current server-request architecture: the vault.save_login
server->client request now opens the two-step card from
createServerRequestHandler (request.cancel tears it down), the answer
travels as one JSON ValueResult string, and an empty step / Esc / expiry
declines with an empty value so the blocked wait resolves instead of
burning its 180s deadline.
The MSIX package carried a single Square44x44Logo.png, so every taskbar and
Start slot on a HiDPI Windows 11 machine was Windows upscaling a 44px bitmap,
and with no altform variants Windows plated the icon itself. The generator now
emits the qualified catalog Windows resolves by itself: <Logo>.scale-{125,150,
200,400} for all four manifest logos at Microsoft's pixel sizes, and
Square44x44Logo.targetsize-{16..256} in the three required theme forms —
unqualified and altform-lightunplated carry the light tile, altform-unplated
(dark theme) the dark tile, the same split macOS dark mode gets. before-build
stages the whole assets/appx directory; app-builder-lib runs makepri and
`makeappx /l` as soon as it sees a qualified asset, so nothing else changes.
The hand-listed *-dark.png logos, generated but never staged, are gone.
Exposed along the way: the commit badge sat outside the portrait's silhouette
clip, and at 16–30px its anti-aliased edge stacked coverage on the plate's
corner pixels and changed the alpha channel between build flavors. The badge
now shares the clip, is plain black, and is left off direct renders below
32px where no SHA is legible anyway.
transports.types.ToolCall exposes call_id as a read-only view of provider_data,
so setattr raised AttributeError in validate_tool_calls for Responses-transport
batches. Write the backing value instead so id and call_id stay in agreement,
and cover both real transports end to end.
Refs #130363
Gate provider-id normalization on the effective id that serialization and
result pairing use (stripped; blank call_id falls back to id), and re-run it
after dedup/capping so a batch that becomes all-provider-prefixed only after
filtering is still normalized before it is staged. Idempotent; mint time only.
Refs #130363
Rewrite the call half of id and call_id independently so a composite id keeps
its response-item half when call_id is bare, and hash with surrogatepass so a
lone surrogate neither raises in validate_tool_calls nor collapses distinct ids.
Adds an end-to-end test through validate_tool_calls (real duplicate repair) and
assistant-row serialization.
Refs #130363