Commit Graph

2597 Commits

Author SHA1 Message Date
ethernet
c179041466 feat(icons): macOS 26 layered icon whose border follows Apple's mask
macOS 26 clips every app icon to its own squircle and shows legacy .icns art
inside a glass plate. Our border is a stroke on the tile's rounded rect, so on
26 the ring sits inside a grey rim and thins toward the corners where Apple's
curve and ours diverge.

Add an Icon Composer package (apps/desktop/assets/icon.icon) of canvas-filling
layers the system masks itself: a border band whose outer edge is the canvas
(the mask supplies the outline) and whose inner edge is Apple's mask offset
inward by the brand border thickness, so the band is the same width everywhere
along the visible outline; and the girl (+ commit badge) as a second layer.
Build flavours recolour the fill in icon.json only.

Apple's mask is a circular arc flanked by two cubic easing segments (the
Figma corner-smoothing construction). Fitted to macOS 26.6.2's own render of
a system icon on the 824-on-1024 grid: radius 214px, smoothing 0.645, rms
0.32px / worst 0.81px; a superellipse cannot get under 0.54px rms.

The .icns targets are unchanged: Apple's own 26-era .icns still sit on the
824 grid, and macOS <= 15 keeps showing that artwork.

The new contract test probes the border layer 640 points along the mask:
opaque at thickness-1.5px, transparent at thickness+1.5px. Rendering the
layers with the old r=185.4 corner violates it at 1448 probe points.
2026-10-02 10:33:43 -04:00
teknium1
f43b70f25d fix(compat-audit): treat with suppress(Exception) as an import guard
contextlib.suppress swallows a failed import exactly like a non-raising except
arm; the audit marked such imports bare, so a module only ever imported under
suppress(Exception) by updaters could never be deleted.
2026-10-02 04:39:28 -07:00
teknium1
6174e342e1 fix(tests): a nested test runner must not inherit the outer runner's slice
The runner's own probe tests start runners from inside a test process,
which inherits HERMES_TEST_SLICE from the CI slice job. Slice 1/2 then
ran half of each probe's file set and slice 2/2 ran none of a one-file
probe ("probe never wrote handoff file"). Read the slice once and drop
it from the environment.
2026-10-01 23:49:20 -07:00
teknium1
093f8d4480 ci: run the Python suite and the macOS lane as two slices each
The full suite was the last job to finish in 33 of 43 CI runs since it
moved from a 96-core to one 32-core runner (p50 666 s). That runner is
~90% packed, so only more machines help, and one bigger box is poor
value: on 96 cores the same files took 25-50% longer each and the suite
ran ~1.8x faster for 3x the price. Two 32-core slices scale linearly
(simulated from per-file CI timings: 262 s / 252 s vs 514 s on one box).

macOS (free 3-core runner, ~7.5 min) would become the slowest required
job once Linux splits, so it runs as two slices on the same free runner.

--slice now partitions from the checkout alone (sorted round-robin):
each slice job restores its own duration cache, and a main push saving
a newer cache between the two restores made timed LPT compute different
partitions per job, silently dropping or double-running files.
Round-robin lands within 5 s of timed LPT at this suite size. Each slice
saves its own cache key on main; the runner keeps the restored entries
for files outside its slice, so the cache stays complete.
2026-10-01 23:49:20 -07:00
teknium1
7f7e1ab402 fix(desktop-update): any failure after the banner keeps a killed update failed
The 124 remap was vetoed only by "Update not complete", but the restart
and verify phase prints other failure lines after "Update complete!":
"Update incomplete — …", "✗ <unit> failed to come back/stay running
after restart", "verification incomplete". Any of them now keeps the
watchdog kill a failure. Only text after the last banner is inspected,
and ✗ is matched as \u2717 because Windows PowerShell reads the BOM-less
script as ANSI.

Reported by @unsupportedpastels in review.
2026-10-01 22:35:29 -07:00
teknium1
91e8f7595d refactor(desktop-update): keep only the reachable 124-after-completion remap
The Windows hand-off's idle watchdog is the only producer of exit 124 on
main (posix.sh has no watchdog and `hermes update` never exits 124), so
the posix remap was dead and goes. The retry-state helper and its
self-test arm collapse into one Resolve-HermesUpdateOutcome check, now
proved end to end through the real hand-off instead: a fake update that
prints its banner and parks silently past a 3 s ceiling surfaces as a
verified success with the gateway fleet restored.

The remap also refuses a run whose output already carries the
"Update not complete" verdict, which supersedes the banner.
2026-10-01 22:35:29 -07:00
Brooklyn Nicholson
1b0d8aa3d5 fix(desktop-update): completed update killed with exit 124 is terminal, never re-run (#96205)
Port of #96340 (Finn763) onto the current legacy-scoped retry gates,
narrowed to the watchdog sentinel only: exit 124 + the "Update complete!"
marker maps to success so the hand-off verifies and relaunches the new
build instead of re-running the completed update (the 70+ minute popup
retry storm). A plain exit 1 after the marker is the stale-gateway
verdict (hermes_cli/update_receipt.py) and must keep failing.

As first written this also added a posix.sh gate and a retry-state
helper with a -SelfTestRetryState arm; the next commit drops the posix
gate (nothing there exits 124) and replaces the helper and self-test
with one end-to-end test through the real hand-off.

Co-authored-by: Finn763 <165816600+Finn763@users.noreply.github.com>
2026-10-01 22:35:29 -07:00
teknium1
d802c47238 docs: one complete plugin catalog submission guide, synced with the README rules
Submitters only saw a stale 5-point summary on the catalog page (it even listed
a 'Released' requirement nothing enforces), while reviewers applied ten README
rules plus criteria written nowhere public (credentials, approvals, disclosure,
compatibility metadata, forks of bundled plugins).

- plugin-catalog/README.md: rules 11-15 write down those review criteria; the
  rule list sits between admission-rules markers.
- New developer-guide page 'Submitting to the plugin catalog': before you
  submit, the PR flow, the full rule block (verbatim mirror), pin updates,
  delisting vs removal, asking for a hook.
- The /plugins 'Submit' links and the user-guide section point at it.
- tests/website/test_catalog_rules_mirror.py fails when the two rule blocks
  differ; classify_changes makes a README-only edit run it.
- AGENTS.md, plugins/AGENTS.md and CONTRIBUTING.md reference both files.
2026-10-01 21:58:11 -07:00
teknium1
b909c01d07 fix(ci): scope the catalog data-only guard to entry PRs; diff the merge commit shallowly
- Run the data-only guard once, inside the changed-entries step of the
  pinned-source job, and only when an entry YAML changed. README, policy,
  docs and CI PRs that touch plugin-catalog/ without an entry no longer go
  red.
- Allow contributors/emails/** beside plugin-catalog/**: the attribution
  fix we push onto catalog PR branches is data the gate never executes
  (29 of the last 134 merged catalog PRs carried one).
- Diff GitHub's merge commit against its first parent with fetch-depth: 2
  instead of a full-history clone plus merge-base. The structural job,
  which only validates the whole directory, goes back to a shallow
  checkout with no diff at all.
- `subdir:` with no value means the repo root in both the parse step and
  the structural validator, matching how the runtime loader has always
  read it.
2026-10-01 10:46:45 -07:00
beardthelion
8444e9eb18 fix(ci): confine plugin-catalog admission gate to the pinned clone
The pinned-source job joined a PR-controlled subdir onto the clone root
with no validation, accepted file:// repos and ref names as the "pin",
and followed committed symlinks out of the clone, so the gate could
validate attacker-planted files instead of the declared commit's
contents. Harden the step end to end:

- validate repo (https://, whitespace-free), sha (40-hex, commit object),
  and subdir (plain relative path) in the parser before any use
- run the parser under python3 -I against installed ruamel.yaml so the
  PR checkout cannot shadow its imports, capture parser output, and
  reset shell variables per entry so a dead parser cannot replay the
  previous entry's fields
- confine PLUGIN_DIR and every symlink beneath it to the clone root
- include renamed and typechanged entry files in the changed set
- feed entries over fd 3 and keep plugin-executed commands off stdin so
  validate cannot drain the remaining list
- NUL-delimit both self-updater grep stages and sanitize the echoed
  filename list so crafted names cannot forge workflow commands
- mirror the same repo/subdir rules in the runtime catalog loader and
  the site extractor, and use \Z anchors so a trailing newline cannot
  split the verdict between gates
2026-10-01 10:46:45 -07:00
Brooklyn Nicholson
5690810779 fix(build): await the scratch-dir removal; drop an unused test import 2026-10-01 00:15:05 -05:00
Brooklyn Nicholson
dde4bc7bb7 fix(update): serialize source-completion tails on the shared update lock; skip Windows file-manager metadata in freshness hashing (#123376, #122803)
WIP for the freshness-guard-os-metadata unit: the UpdateLock claim in
complete_source_checkout plus its tests are complete but unrun; the
Thumbs.db/Desktop.ini skip predicate is in but its test rows are not yet
extended, and the rmTree ENOTEMPTY test has not been written yet.
2026-10-01 00:15:05 -05:00
PRATHAMESH75
64eca050ec fix(build): skip macOS Finder metadata in build freshness hashing (#122632)
Opening the checkout in Finder mid-build lets .DS_Store, AppleDouble ._*
sidecars or .localized land inside a hashed input dir (apps/desktop, apps/shared,
assets, scripts/build). treeHash then sees a new input and recordProduct throws
"Build inputs changed during compilation; retry the build", so `hermes update`
rebuilds the desktop app, fails closed, and never prints Update complete.

Finder metadata is never a build input, so skip it as a baseline in treeHash —
covering source, output and prepared trees uniformly rather than only the source
predicate. A real source change is still detected and still names nothing new to
hash, so clean checkouts (CI) hash byte-identically.
2026-10-01 00:15:05 -05:00
Brooklyn Nicholson
4091ef8967 fix(build): absorb Finder's ENOTEMPTY scratch-dir removal race (#122803)
macOS's Finder/Spotlight can drop a fresh .DS_Store (or xattr) into a
directory while we're removing it, making rmSync throw ENOTEMPTY on the
first pass. That file is cosmetic garbage — retry the removal so the
race can't abort an otherwise-clean product build.

Salvaged from #123078 (its freshness.mjs hunk is subsumed by the broader
metadata skip; the unrelated CI-workflow deletions in that PR are not
included).

Co-Authored-By: dlshumway <85191357+dlshumway@users.noreply.github.com>
2026-10-01 00:15:05 -05:00
Halldrix
eec5d2af64 fix(build): record the baked stamp clock, not the live one (#123308)
write-build-stamp.mjs rewrites the stamp's `builtAt` clock on every build, so the
whole-file hash moved under a receipt that had just recorded it. Two builds
overlapping on one tree failed with "Build inputs changed during compilation" for
a difference the build machinery itself made, and the caller turned that into
SystemExit(1).

Hash the stamp's provenance identity instead, key-sorted so a cosmetic
re-serialisation is not a change. The clock is excluded from that comparison but
recorded as `stampClock` — the value the bake actually embedded, captured from
the same read that produces __HERMES_INSTALL_STAMP__ — because packaging copies
the live stamp into the bundle after the receipt is written. Re-reading the stamp
at record time would certify a clock the output never contained, and
productCurrent would then report a bundle current whose packaged clock disagrees
with its baked main, which is exactly what detectBundleSwap must never do.

recordProduct still compares the prepared inputs, so a clock-only restamp cannot
throw. productCurrent additionally requires the recorded clock to match, so a
restamp between bake and packaging is a rebuild rather than a silent divergence.
A receipt predating the field compares by hash alone until the next build
rewrites it; the release packaging path never consults productCurrent.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-10-01 00:15:05 -05:00
Halldrix
ff28c475e3 fix(build): hash the desktop stamp's identity, not its build clock (#123308)
write-build-stamp.mjs rewrites `builtAt` on every build, and build.mjs runs it
before compilation, so the desktop build rewrote a file its own receipt hashed
as a prepared input. Two concurrent `hermes desktop` builds therefore made
recordProduct() fail with "Build inputs changed during compilation" on a change
the build itself caused, and the caller turned that into SystemExit(1).

Hash the stamp's provenance identity instead of its raw bytes. builtAt is the
only field that moves between two stamps (the tree documents it as
"differs on every rebuild"), and it is a build clock, not provenance: the baked
output changes when commit/payload/tag change, never when time passes. A
non-JSON stamp still falls back to the whole-file hash, so content keeps
deciding freshness and only the clock is ignored.

Unlike b3618bda35, the stamp cannot simply leave the hash set:
bundleElectronMain bakes its bytes into electron-main.mjs, and
bundle-swap.test.ts pins "same commit rebuilt (builtAt moved)" as a real swap,
so freezing the clock would break a committed test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-10-01 00:15:05 -05:00
Siddharth Balyan
09681e7959 fix(release): --branding stable works before stable has a channel record (#129541)
`--branding stable` copies the identity from the published stable channel
record on R2, and refused when there was none. Production has no
releases/channels/stable.json yet: the channel pipeline merged after the
last stable release (v2026.9.24), and no stable release has published
through it since. So every stable-branded channel build failed at
allocation ("none is published"), including the first real dispatch.

When the stable record does not exist, use product_identity(), the
identity channel_publish already holds every stable release to. A stable
record without a head still refuses, so once the first stable release
publishes, its record is the only authority again and this path stops
running by itself.

The allocation step needs node for product_identity(), so the workflow's
identity-reader setup also runs for branding == 'stable'.
2026-10-01 01:30:24 +05:30
Siddharth Balyan
86f54bfdaa Flavored desktop builds no longer stamp themselves dirty (#129540)
* fix(bundles): write the build stamp before flavored icons land

A flavored build (commit, canary, preview channel) copies its icons over the
committed apps/desktop/assets before packaging. write-build-stamp ran after
that copy, so its git check saw modified tracked files and stamped the
bundle dirty, and the Windows smoke refused it (stamp.dirty: true != false),
which keeps a channel from promoting the build. Stamp first, in both the CI
driver (scripts/bundles/desktop.py) and the source build (build.mjs). The
stamp reads no artwork, so its content is unchanged.

* test(bundles): pin the build stamp ahead of the flavored icon copy

buildSourceDesktop copies flavored icons over committed files in
apps/desktop/assets. write-build-stamp asks git whether tracked files
changed, so it has to run while the committed bytes are still there. The
test records what the stamp step sees through the injectable run seam.
It fails on the old order (sees the flavored icon) and passes on the new one.
2026-10-01 01:30:18 +05:30
emozilla
5c08ad68f7 ci: run the slow lanes on a PR only when it touches their paths
The E2E suites, the Windows E2E jobs, the Docker image build and the Nix
flake check took about 120 of the ~140 larger-runner minutes each PR push
spent, and fired on nearly every PR. The 32-core pool backed up behind
them.

On a pull request each slow lane now runs only when the diff touches the
suite, its harness, or the code it guards (classify_changes.py
_E2E_LANES, _DOCKER_PATHS, _NIX_LANE_PATHS), or when the PR carries the
run-e2e label. Every push to main still runs all of them, and a release
run still forces every lane on.

Adding run-e2e reruns the head commit's CI, Docker and Nix runs; detect
reads the live labels because a rerun replays the original event.

Replayed over the last 200 merged PRs, the modelled 32-core cost of one
push drops from about 120 to about 10 runner-minutes.
2026-09-30 14:37:52 -04:00
ethernet
13dbe260aa feat(release): --branding stable builds a channel commit as the regular app
A preview channel always minted its own side-by-side identity (Hermes NAME,
ai.hermes.channel.h<token>), and a plain --build-commit carries
Hermes Agent <sha7> with a red icon, so there was no way to ship an exact
commit under the official name, icon and package ID.

--branding stable (with --channel) makes channel creation copy the identity
from the published stable channel record instead of reserving a new token.
Branding is fixed at creation like the rest of the identity: re-dispatching
with the other branding is refused, as is stable branding before a stable is
published or in a disposable namespace. --branding without --channel is
refused rather than silently falling back to a sha-branded one-off build.

The workflow gains a `branding` input, forwarded to the allocation step and
the dispatch log so the printed release.py replay matches what was sent.
2026-09-30 09:19:35 -04:00
JoaoMarcos44
16b214e1a1 fix(config): make destructive replacements explicit
atomic_config_write() now refuses deletion-by-omission at any mapping depth:
atomic_roundtrip_yaml_save treats keys absent from the payload as deleted
recursively, while the old docstring described the call as a merge, so a
partial dict could silently erase unrelated config (#125107). A new
atomic_config_replace() names the deliberate full-state replacement contract
explicitly, and the owners that intentionally remove state (save_config,
provider reset/switch, credential mirror scrub, doctor key migration, profile
channel stripping, TUI full-state saves) route through it. Additive writers
stay on the guarded atomic_config_write.

Salvages #125152 (identical patch, cherry-picked; conflict in
_write_user_config resolved to pass atomic_config_replace through the new
telemetry wrapper) and carries the executable bit main expects on
scripts/check_config_yaml_writers.py.

Fixes #125107

Co-authored-by: JoaoMarcos44 <joaomarcosdias444@gmail.com>
2026-09-29 20:24:19 -04:00
ethernet
e336fbcf79 fix: release cut output waits for its CI run and leads with the next action
The cut printed before GitHub listed the dispatched run, so it almost always
said "the run is not listed yet". It now polls up to 30s for the run, and when
none lists it links the stable-release workflow page. The copy is shorter:
Release CI, the skipped-tests warning (bold on a tty), the draft link, and the
publish command on its own line.
2026-09-29 15:12:04 -04:00
ethernet8023
792ba0a264 fix(release): read a tag's record, not its signature
git keeps an annotated tag's signature inside the message body, so
`git tag -l --format=%(contents)` returns the JSON record followed by an
armored block, and `json.loads` of the whole message raises "Extra data: line 2
column 1" at the armor's first byte. Claim tags, final receipts and build
receipts are created on a host whose git signs tags, so stable admission
(`python -m scripts.releases.stable admit`), ordered reconciliation
(`python -m scripts.releases.sequencer`) and tagless build receipts each
refused to read their own tag.

Add `versioning.tag_record()` as the one reader of a tag's record — the text
before the armor, unchanged for an unsigned tag or a detached signature — and
route every `%(contents)` read through it in stable.py, sequencer.py and
commit_build.py. commit_build imports it inside the function so its module
surface stays stdlib-only for the isolated checkout-admission step.

The reads inside the release tests use the same reader, and the one fixture
that creates a plain final tag says so explicitly, so the release suite passes
on a host whose git signs tags by default.
2026-09-29 14:55:15 -04:00
ethernet
36699ac97a Merge pull request #128156 from NousResearch/fix/desktop-flavored-icons-clean-checkout
Desktop release builds stop leaving their flavored icon render in the checkout
2026-09-29 13:08:09 -04:00
ethernet8023
4bc38bceec fix(desktop): hand the admitted checkout back after flavored icon staging
electron-builder resolves every piece of packaging artwork from the workspace
— the app icon, the icon.ico extraResource, the MSIX logo set before-build
stages into build/appx, and the exe identity stamp — so a flavored canary or
commit build copies its rendered icon set over apps/desktop/assets before
packaging.

Those files stopped being gitignored in 4b7229d612, which turned a write that
was invisible into 15 dirty paths. The source custody check then refuses the
checkout twice: inside the same build, at the check that used to sit after the
copy, and on every later build from the first check on a now permanently dirty
tree.

Hold the render at the workspace path for the packaging window only and
restore the admitted files afterwards, whether packaging finished or failed.
The final custody check now asserts the build handed the checkout back.
2026-09-29 12:29:47 -04:00
Mark S.
ca705dbf7e fix(ci): card-tool contract test reads the as-const list and runs on desktop edits (#128010)
#127828 changed CARD_TOOL_NAMES in apps/desktop/src/lib/tool-render-class.ts
from `new Set([...])` to `[...] as const`. The pytest that pins that list
against the gateway's _TOOL_LIFECYCLE_UI_TOOLS located it with a regex that
only matched `new Set([`, so it has failed on main since with
"CARD_TOOL_NAMES not found in tool-render-class.ts". The gateway set still
covers every desktop card name; only the test's parser was stale.

The PR merged green because it touched only apps/, which skips the Python
lane, so the one test that reads that file never ran on it.

- The parser accepts both declaration forms and fails on an empty list
  instead of passing a vacuous subset check.
- tool-render-class.ts joins _PY_RELEVANT_CONTRACT_FILES so an edit to it
  runs the Python lane, with a classifier case for the entry.
2026-09-29 12:27:26 -04:00
ethernet
74163fa18c fix(desktop): pass release epoch to native version subprocess 2026-09-29 10:55:18 -04:00
kshitijk4poor
596678d665 refactor(update): tighten the partial-clone heal after review
- _deepen_shallow_repo marks packs in its finally without a git config probe
  that could itself raise and turn a finished unshallow into False; markers
  are inert in a clone without a promisor remote.
- Installers warn instead of aborting the stage when a marker cannot be
  written, matching the Python helper.
- Docs: the manual commands follow HERMES_HOME, and the missing-object
  backfill feeds IDs through `fetch --stdin` so a large list cannot exceed
  the Windows command-line limit.
- Test and docstring comments describe the contract, not the incident.
2026-09-29 15:24:15 +05:30
kshitijk4poor
7b20b72269 fix(install): installer rerun heals a partial clone stuck on the pack-objects crash
An install whose updater dies on the git 2.53+ pack-objects BUG (#124272)
never fetches the release that heals it: the fetch runs in the installed
code. The installer rerun (install.sh / install.ps1, served fresh) is the
path that runs new code, so when origin is a promisor remote it now marks
the checkout's unmarked packs before its existing-checkout fetch. Marking is
idempotent and never rewrites objects, so it runs up front rather than as a
retry that would first print a failed-fetch report.
2026-09-29 15:24:15 +05:30
teknium1
9b7db3105b fix(telemetry): fewer rows per busy day and a smaller local store
A volume simulation through the real store showed two costs worth cutting.

Rows: hermes.task_run.finished carried duration, retry, model-call and
tool-call buckets on top of outcome/end reason/surface, so almost every task
became its own row; hermes.tool_call.count did the same with latency and
retry buckets. The terminal rows now keep only the fields they are filtered
by, and the same end events feed two small split counters:
hermes.task_run.duration (surface, outcome, duration, retries) and
hermes.tool_call.latency (tool category, latency, retries). Per-task call
counts already ride on hermes.task_cost.count. v3 still accepts the v2 field
sets of both counters so rows recorded before an upgrade drain.

Local store: every package lived three times for 30 days (counter rows, the
payload column, a pretty-printed outbox file). Outbox files are now compact
JSON, and once the ingest has accepted or refused a package the database
drops its copy of the body; the file stays as the local history the docs
promise, and pending packages keep their body for resends.

Measured (simulated day, skewed draws, before -> after):
  heavy    2,788 -> 2,347 rows, 21.0 -> 17.7 KB on the wire, 102 -> 44 MB local over 30 days
  extreme  6,561 -> 5,047 rows, 46.0 -> 33.6 KB, 272 -> 102 MB
  typical    663 ->   650 rows,  7.1 ->  6.8 KB,  24 ->  13 MB
2026-09-28 12:43:03 -07:00
teknium1
0b6ac0b622 fix(telemetry): live relay smoke no longer expects adoption from an agent-created skill
The signals fix (M15) stopped counting Hermes' own skill creation as user
adoption, so the smoke's agent-created skill records no feature_adoption
row; the exact name sets now assert its absence.
2026-09-28 12:43:03 -07:00
teknium1
d6e4e33d7d fix(telemetry): live relay smoke expects the v5 efficiency and adoption rows
The smoke's exact metric-set checks (SQLite store and export packages) were
never updated for v5: every attended CLI turn now also emits task_cost,
tool_output_truncation, tool_overhead and tool_enabled_unused, and the skill
lifecycle step latches feature_adoption(skills_created). A correct run failed
with "Unexpected SQLite counters", so the documented live gate could no
longer catch a real regression. Both sets now include the five metrics and a
shared _validate_v5_rows asserts their dims in the store and the packages
(task_cost provider/model `custom` for the canary model, api_calls 2,
tool_calls 1; read_file truncation; file toolset used; tool_overhead on cli).

Live smoke (loopback fake OpenAI server, no model spend):
  before: rc=1 AssertionError: Unexpected SQLite counters
  after:  rc=0 "Hermes -> NeMo Relay shared-metrics smoke test passed"
Sabotage: expecting the canary model id in task_cost is caught.
2026-09-28 12:43:03 -07:00
teknium1
c5555efca8 test(smoke): expect the per-response model_reply_issue counter
Every primary model response now records hermes.model_reply_issue.count
(issue=none for usable replies), so the smoke turn's two scripted replies
add one row with value 2; the exact-metric-set assertions must include it.
2026-09-28 12:43:03 -07:00
teknium1
b1e9c2a5a1 test(smoke): expect v4 tool-quality, context-peak and startup counters 2026-09-28 12:43:03 -07:00
teknium1
ccd1a274e8 fix(telemetry): user-named providers and models never leave the machine; milestones use the owning profile
Independent review of the v3 metrics found:

- Provider fields were shape-checked only, so `custom:<config key>` (and
  any unshipped provider id) reached setup.completed, install.snapshot,
  model_switch, fallback, model_tokens and model_route. Providers now pass
  only when Hermes ships them (auth registry, overlays, model catalog,
  aliases, cached models.dev ids); everything else reads `custom`. A custom
  or loopback provider's model id reads `custom`, as does any model id that
  looks like a path or URL. Two existing tests asserted the old export of a
  custom endpoint's model id and an unshipped provider; they now assert the
  collapse, and the smoke run treats the model canary as prohibited.
- Install milestones read the install age on the Relay thread, which has no
  profile binding, so a multiplexed profile got the launch profile's age.
  The subscriber captures its home at construction.
- Gateway sessions retired by the store (auto-reset, /new, resume) now close
  their metrics session at the route transition instead of at shutdown.
- The dashboard MCP add records its install after leaving the config lock.
- Compression and gateway slash counting helpers move into
  shared_metrics_events so the >2k-line files barely grow.
2026-09-28 12:43:03 -07:00
teknium1
3d62ae2233 feat(telemetry): decision-data docs, consent copy, smoke coverage and install-age for new installs
- Docs: a Decision-data metrics section mapping each metric to the product question it answers;
  skill-load and snapshot paragraphs describe the new public-name and install-age fields.
- setup consent copy names the new data classes (session length, token totals, command and
  catalog names, bucketed setup counts).
- The real-CLI smoke now asserts the session summary, token sums, TTFT bucket and milestones in
  both the SQLite store and the exported, schema-validated package.
- A profile with no sessions yet is a brand-new install (lt_1h), not unknown: the first task's
  milestone and the first snapshot fire before state.db has a session row.
- Two invariant tests: session summaries + once-per-install milestones; token sums per model and
  auxiliary task with private task names collapsed.
2026-09-28 12:43:03 -07:00
teknium1
b8732b768e feat(telemetry): shared metrics v3 with failure classes, tool usage, platform and daily snapshot
The fleet rollups could say that tasks and tools failed, but not why, on which
model, on which messaging platform, or which features installs actually use.

- model_route rows gain call_role, outcome and error_class (the classifier's
  FailoverReason for the last failed attempt; a success row keeps the error it
  recovered from).
- task_run rows gain platform (built-in gateway platform, plugin, or none);
  task_run.finished gains failure_class from a closed set.
- new hermes.tool.usage.count: tool_name (only names from the static
  toolsets.TOOLSETS snapshot; mcp/plugin otherwise), outcome, error_class.
- new hermes.install.snapshot, latched to once per rolling 24h like
  client.active: memory provider plus bucketed counts of MCP servers,
  plugins, skills, cron jobs and profiles; no names.
- package schema v3; v2-shaped pending counters still validate and drain.
- consent copy and developer docs list the new categories; the relay smoke
  asserts v3 shapes and disables title generation (its third model request
  broke the smoke's request-count check on main).
2026-09-28 12:43:03 -07:00
Teknium
a5bd246865 Old pre-decomposition import paths are gone: plugin compat layer removed on schedule (#126164)
* refactor(plugins): remove the Sep 2026 decomposition compat layer on schedule

The PLUGIN-COMPAT layer (2776813df3 + d63e380324 + 0a5164cebe) kept pre-#102117 import paths
alive for external plugins until 2026-09-14. That window closed two weeks ago; since then the loader
has already been skipping plugins that use the old paths. This removes the layer itself:

- 328 appended `PLUGIN-COMPAT` blocks (lazy `__getattr__` pointer tables, re-exported third-party
  names, restored dead definitions) and the three re-export stub modules
  (gateway/startup_watchdog, hermes_cli/observability/relay_runtime, tools/environments/modal_utils)
- COMPAT_MANIFEST.md, compat_manifest.json, scripts/check_compat_pointers.py and its lint step
- the reporting surfaces: CLI banner notice, `hermes plugins compat`, the `hermes doctor` section,
  the post-update notice, the Desktop one-time dialog, the loader's pre-import skip and the
  `plugins.allow_deprecated_imports` escape hatch

An external plugin that still imports an old path now fails to load with its ImportError as the
reason in `hermes plugins list`, the same path as any broken plugin.

hermes_cli/plugin_compat.py stays as three inert stubs (compat_report, removal_in_effect,
summary_lines): an already-running pre-removal `hermes update` lazy-imports them after the checkout
swap (tests/compat/old_updater_surface.json).

In-tree fallout, both already dead: hermes_cli/setup.py::_check_espeak_ng (no callers; its
`shutil` came from a compat block) and gateway/config.py::SessionResetPolicy ("retained solely for
the scheduled plugin-compat window"). Two test_run_agent patches targeted the removed
`run_agent.handle_function_call` pointer; they now patch `model_tools.handle_function_call`, the
seam production reads, like every sibling test in that file.

* chore: retrigger CI (zero-job startup_failure phantom)

* test: drop resolution allowlist rows for the two deleted which() sites

hermes_cli/setup.py::_check_espeak_ng (dead) and tools/skillevaluator_scan.py::scanner_available
(a restored definition inside a PLUGIN-COMPAT block) no longer exist; the stale-row gate requires
their allowlist entries go with them.
2026-09-28 10:21:41 -07:00
alt-glitch
1e2498a23d fix(whatsapp): stamp every bridge console line in the local log shape
The adapter captures the bridge's stdout and stderr verbatim into
bridge.log, so a console line without a stamp cannot be placed in time.
The previous commit stamped 12 of the bridge's console calls one by one
with a bracketed UTC ISO stamp; the other calls (config summary, pairing
mode, reconnect-scheduler and version-resolver messages, inbound media
download failures) stayed bare.

This replaces the per-call wrapping with one installConsoleStamps() call
at bridge startup that prefixes every console.log/warn/error line with
`YYYY-MM-DD HH:MM:SS,mmm ` in local time, the shape the Python logs in
logs/ already use. The 12 call sites go back to plain console calls.

Lines that programs parse must stay bare. The dashboard pairing watcher
runs json.loads on each stdout line of `bridge.js --pair-json`, so the
JSON event lines (pair events, debug, poll_update_decode and `ignored`
events) now go through writeJsonLine(), which writes to process.stdout
directly. The terminal QR block is written the same way so its first
row is not shifted. Pino (the Baileys logger) writes to fd 1 itself and
never passes through console, so its lines are unchanged.

Refs #97021
2026-09-28 18:21:25 +05:30
ygd58
ee2c8b52a6 fix(whatsapp): timestamp bridge.js's lifecycle console.log/warn lines
Fixes #97021.

The WhatsApp bridge's human-facing lifecycle lines -- startup
("bridge listening", "session stored"), connection state (connected,
logged out, restart/reconnect), and "[bridge] ..." warning lines --
were bare console.log/console.warn output with no timestamp. The
platform adapter pipes the bridge's stdout/stderr verbatim into
bridge.log, so no timestamp is added downstream either. Only
structured JSON events (pair events, allowlist rejections from #92683)
carried a timestamp. During incident forensics this made bridge.log
impossible to sequence on its own -- the exact durations of a
relaunch/re-pair loop, or the ordering of "Logged out" relative to a
fatal stream error, couldn't be reconstructed without cross-correlating
gateway logs and the sparser pino output.

Added timestampedLine() to bridge_helpers.js (the existing pure,
testable helper module bridge.js already draws from for reconnect
scheduling and version resolution) -- a small function that prefixes a
message with an ISO-8601 UTC timestamp in brackets, matching the
issue's own suggested fix direction and kept deliberately distinct
from the `ts: Date.now()` epoch-ms convention #92683 already
established for the JSON event stream (these are plain human-readable
lines, not JSON payloads).

Applied it at the exact lifecycle line sites the issue's own line
inventory cites: the core startup lines ("bridge listening on port",
"session stored in"), the connection lifecycle lines (connected,
logged out, restart-code-515, reconnect-in-3s, pairing complete), and
the five "[bridge] ..." warning lines (poll update/upsert aggregation
failures, gif/ffmpeg conversion fallback, failed read receipt).
Scoped narrowly to what the issue asked for -- did not touch the
already-structured JSON event lines, or the separate config-summary
startup block (allowed users / DM policy lines) the issue didn't cite.

Added a new test file (bridge_helpers.timestamp.test.mjs), following
the established plain-assert, no-framework pattern from the existing
bridge.reconnect.test.mjs: verifies the prefix is a valid, parseable
ISO-8601 timestamp; the original message text (including
template-literal-interpolated content, matching the actual call sites)
survives byte-for-byte after the prefix; and two calls a moment apart
produce non-decreasing timestamps, confirming the prefix reflects
actual call time rather than a cached value -- so a relaunch loop's
individual lines stay independently sequenceable, the issue's core ask.

All assertions pass in the new test file. Ran the existing
bridge.reconnect.test.mjs, bridge.sendqueue.test.mjs, and
allowlist.test.mjs directly -- all pass unchanged (no regression to
bridge_helpers.js's other exports or to bridge.js's own logic, since
this only wraps pre-existing message strings passed to console.log/
console.warn without changing any control flow).
2026-09-28 18:21:25 +05:30
teknium1
8a3ede1be0 fix(bundle): payload smoke runs the harness with telemetry off, like runtime
browser-harness sends each CLI event from a detached python child; in the
smoke it outlived the check and held the relocated payload open on Windows,
so the restore rename failed with EPERM. Hermes's own harness env sets
ANONYMIZED_TELEMETRY=false; the smoke now does the same.
2026-09-27 23:53:40 -07:00
teknium1
2843d8eea4 test(bundle): smoke the payload's browser-harness on the store interpreter
The relocated-payload smoke now runs browser_exec's engine the way
tools/browser_use_cli.py launches it (store interpreter, -S, only the
payload site dir on PYTHONPATH), so a payload that cannot start the
default browser driver fails the release build instead of shipping.
2026-09-27 23:53:40 -07:00
teknium1
1d287d5375 fix(browser): ship the Browser Use CLI engine in every install, Desktop included
The default browser_exec tool ran the `browser-use` CLI from a PM side
environment (browser-use==0.13.10 in <home>/environments/browser-use),
provisioned by the installers and `hermes update`. Sealed Desktop payloads
skip that step, so the Desktop app never had it and silently fell back to
the built-in tools; the side env was also per-profile and 225 MB.

The CLI's execution path is only `browser_harness.run.main()`; the
browser-use agent framework (anthropic/openai/google-api pins, 93 MB of
googleapiclient) is never imported. browser-harness itself is 2.6 MB of
pure Python whose pins (Pillow 12.3.0, websockets 15.0.1) already match
Hermes's own, so it becomes a core dependency and runs on sys.executable:

- pyproject/uv.lock: browser-harness==0.1.13 (+ cdp-use, fetch-use).
- _find_cli() returns [sys.executable, -m, browser_harness.run]; the child
  env points PYTHONPATH at the harness site dir (the Desktop store
  interpreter boots without a venv and the harness daemon re-runs
  sys.executable), replacing whatever the agent inherited.
- The side-env provisioning (install_cli, the update/installer step) goes.
2026-09-27 23:53:40 -07:00
teknium1
b63c138d78 fix: Hermes never falls back to the user's node/npm/npx/uv
Maintainer ruling: only Hermes and only its packaged package managers
(uv/pip/node/npm) are ever used; no PATH fallback when the managed tool is
missing, no "prefer the user's if new enough".

- hermes_constants.find_node_executable: node/npm/npx resolve to PM's
  installed copy or None. Every caller already pm.ensure()s on None, so a
  missing runtime is now provisioned instead of silently borrowing the
  user's Node (native-addon ABI / npm cache mismatches).
- agent/lsp/install._install_npm: pm.ensure('npm') when PM npm is absent,
  instead of failing over to whatever npm is on PATH.
- gateway._append_node_dir_for_service: stop baking the invoker's PATH node
  dir into generated systemd/launchd units.
- main_install_repair._resolve_node_runtime_npm: drop the PATH re-scan for
  another npm.
- source_build.source_product_current: run the freshness reader only with
  PM's node.
- doctor: Node/npm rows and npm audit use PM's copies (Termux APT distro
  keeps its system Node).
- install.sh ensure_uv / install.ps1 Get-Uv: always stage the pinned uv
  artifact; delete the "uv on PATH if new enough" developer shortcut.
2026-09-27 22:04:26 -07:00
teknium1
27062c3474 fix: install cua-driver and the Browser Use CLI by default again
Computer use and browser use are meant to work out of the box. The PM rewrite
(3d12e86ef1) and the MSIX installer rework (47f4ab3a17) dropped the
install-time cua-driver fetch (7060ac7bed) and the Browser Use CLI install
(baa6b2e34d). On a fresh install the computer_use check_fn therefore stayed
False, so the tool never reached the model and its lazy ensure could not fire,
and browser_exec quietly fell back to the built-in tools.

- cua-driver is a default PM package, so the installers, a bare
  `hermes pm install` and `hermes update` carry it on every target it builds
  for. Adds the missing Android gap (the lock has no bionic artifact).
- The shared default-tool step, which the installers (via source completion)
  and `hermes update` both run, provisions the Browser Use CLI for the default
  and explicit Browser Use backends. `--skip-browser` declines it along with
  agent-browser, and `off`/Camofox never use it.
- Installers regain --skip-computer-use / -SkipComputerUse (recorded as
  `--without cua-driver`).
- The update message stops calling every default "browser tools".
2026-09-27 19:18:58 -07:00
happy5318
113a63e853 fix(plugins): make catalog known_issues informational, not a fail-closed gate
Relaxes the fail-closed install gate from #124037 per teknium1 review
2026-09-27 (known_issues informational; guard belongs at mode-selection seam):
- dashboard_install_plugin: no longer refuses known_issues entries — the text
  flows into the result warnings plus a machine-readable "known_issues" key so
  the UI can show it; the memory-provider migration paths (migrate_all_homes /
  recover_at_startup via _install_into) install hindsight again in any mode.
- cmd_install: prints the yellow "Known issue:" lines but drops the TTY
  requirement and the y/N confirmation; non-interactive installs proceed.
- entry_capability_summary: includes "Known issues: ..." so install prompts and
  the catalog UI surface the text.
- scripts/validate_plugin_catalog.py: register known_issues in KNOWN_KEYS.
- tests: keep parse round-trip + install-summary display; drop the live-catalog
  prose pin (test_live_catalog_hindsight_declares_known_issues) and the
  zoneinfo import hack.
- contributors: map 5318happy@users.noreply.github.com -> happy5318.
2026-09-27 17:06:55 -07:00
kshitijk4poor
36c91657da fix(install): kill the PortableGit extractor tree on timeout, drop dead flatten
Process.Kill() on PS 5.1 kills only the stub; its post-install children kept
%TEMP%\hermes-git-bootstrap-<PID> held past the finally cleanup. taskkill /T /F
reaps the tree. The single-wrapper-dir flatten is gone: the sha-pinned
PortableGit roots cmd\git.exe directly (verified against the real archive).
2026-09-28 02:04:08 +05:30
finn763
7a40c28750 fix(pm): execute the pinned git SFX from a scratch copy, not the cache
CI run 36189416163 failed after "git: unpacking": executing the cached
fetch-<sha> PortableGit PE in place left it handle-held (Defender
on-execute scan / the stub's RunProgram child chain) past pm's ~2 s
_remove_entry retry, so download cleanup raised WinError 32. Review
(teknium1, 5 threads) adds the rest:

- Git.unpack now copies the artifact into a .sfx-* dir beside the
  staging tree and executes the copy; pm's .staging-* teardown
  (ignore_errors) owns that path, so any hold lands on a disposable
  path and the cache dir only ever holds read handles
- refuse off-Windows with the cross-host trade-off stated instead of a
  raw PermissionError; documented in package-management.md and the PR
- the extractor is a GUI-subsystem stub that is silent under -y: error
  messages now carry the exit code and the usual causes (disk full,
  path length, antivirus) instead of promising captured output; the
  docstring no longer claims "no GUI" and records that the stub shows
  an Extracting window and runs the vendor post-install
- install.ps1: WaitForExit(600000) + Kill() mirrors pm's timeout=600,
  Fail reports the exit code and the silence
- tests: the OS-refused-exec assertion and the not-in-text change
  detectors are replaced by subprocess-argv invariants (scratch copy
  location, exit-code message, off-Windows guard before any execution)

Related to #122512

(cherry picked from commit adaf76a286bebe30c89aee4174df27f1945b60c9)
2026-09-28 02:04:08 +05:30
finn763
6d9ae2da6f fix(install): stage pinned git without tar/bzip2 (PortableGit SFX)
Windows 10 boxes whose System32 tar.exe cannot run the bzip2 filter die
at stage=prerequisites with "unable to run program bzip2 -d" while
extracting the pinned Git-2.53.0.3 tar.bz2 (#122512). Repin git for
both win32 targets to git-for-windows' PortableGit self-extracting 7z,
which carries its own extractor and the bundled usr/bin/bash.exe:

- pm/lock.json: new artifact urls + sha256 (the pin authority)
- scripts/install.ps1: generated fragment regenerated; Get-PinnedGit
  downloads the SFX and waits on it explicitly (the stub is a
  GUI-subsystem exe, so PowerShell's & does not wait); the System32
  tar invocation and its msys symlink excludes go away
- pm/packages.py: Git.fetch_url/Git.unpack run the self-extractor
  after the sha256-verified download
- pm/store.py: drop the now-callerless git_msys branch of extract_tar
- tests: RED->GREEN test runs the real Get-PinnedGit against a
  bzip2-less System32 tar.exe stub with no bzip2 on PATH; the three
  obsolete tar-contract tests and the install.ps1/PM msys-links parity
  test are replaced by a no-external-decompressor contract test

Closes #122512

(cherry picked from commit 4915304213495d3207ec6cd659e57cd16ef08d60)
2026-09-28 02:04:08 +05:30
kshitijk4poor
44e42b2502 fix(install): re-runs update tag-pinned narrow checkouts instead of aborting
The installer's re-run path fetched `origin <branch>` by name, then checked
the branch out and fast-forwarded to origin/<branch>. On a checkout an older
installer made with `--depth 1 --single-branch --branch <tag>`, the remote's
only refspec maps the tag: the fetch wrote FETCH_HEAD but no origin/<branch>,
and `git checkout main` failed with "pathspec 'main' did not match".

Fetch by explicit refspec (as `hermes update` now does), and when there is no
local branch yet create it at the fetched tip — checkout's own branch guess
ignores remote refs the configured refspec does not map. The remote's fetch
config is left as the user has it. Same change in install.sh and install.ps1.
2026-09-28 01:53:35 +05:30