The light clone now copies plugins/ through the profile copier: bytecode caches,
sockets and the installer's in-flight .install-*/.update-* staging dirs stay
behind, while .install-metadata.json (revision + catalog provenance) travels, so
a catalog-installed memory provider runs at the source's revision in the clone
instead of resolving nowhere. The CLI names the plugins it carried.
Co-authored-by: Eri Barrett <eri@plasticlabs.ai>
When creating a profile with --clone or --clone-from, the source
profile's plugins/ directory is now copied to the new profile
alongside config, skills, and memory files.
Symlinks in the source plugins/ are preserved as symlinks
(symlinks=True). The destination is not overwritten if it already
exists (dirs_exist_ok=True), matching the skills clone pattern.
Fixes plugin hooks not firing on non-default profiles (#50937, #65593)
by ensuring plugins are present in the new profile's plugins/
directory.
get_codex_model_ids returns CuratedFallbackModels directly instead of a
second codex-only marker that _codex_catalog re-wrapped; the class moves to
the static catalog leaf so codex_models can use it. The catalog identity
normalizes its route with normalize_route_base_url like every other route
key.
main already pins the adjacent -900k variant for every verified slug
(test_picker_synthesizes_900k_variants_for_verified_slugs); the copy was
unrelated to the fallback fix.
_anthropic_catalog() returned the curated list as a plain list when the live
/v1/models fetch failed, so a transient outage on a proxy base_url was cached
with the full 1h TTL as if it were the account's/proxy's real catalog, and
neither a same-credentials restart nor Refresh Models could shake it before the
TTL lapsed. Wrap the fallback return in CuratedFallbackModels, the placeholder
type #107391 introduced for the same class of bug.
Salvaged from #121248 (the Anthropic half; the Codex half lands via the
typed-fallback commit before this one).
A failed ChatGPT Codex catalog fetch returned the static offline hints as if
they were the account's catalog, so the picker cached them and dropped
account-gated rows such as GPT-6 Astra until the cache expired. The hints are
now typed (CodexFallbackModels) and cached as short-lived curated fallback
rows that never replace a verified account fetch, and the catalog cache key
includes the resolved route, not only the ChatGPT principal.
v1.2.1 adds the first-run notice for the embedded server download and serialises client creation
so the start worker and the first memory operation cannot each start a daemon.
The page still said multi-local and mixed tool_call batches are rejected.
The agent now splits a batch containing local tools into one call per
entry; document that and the connector-only dispatch.
E2E tests diff in-process get_version_info() against a real hermes process
(Windows boot lifecycle: --version must report this checkout). Drop the
root conftest's seed for the e2e tree; those processes are heavy anyway.
Each test file runs in its own process, and the first User-Agent build in a
checkout without an install stamp runs 7 git subprocesses. In a large local
clone that is ~0.5 s per file (tests/cron: 91 s -> 54 s wall, 610 -> 336 CPU-s).
CI's depth-1 checkout pays only ~7 ms, so the seed uses the identity CI already
produces. Tests that exercise resolution reset the cache themselves.
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.
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.
Adding ci-reviewed starts a job that waits for the PR's CI run, then reruns
its failed jobs. A push during the wait cancels that run, the waiter sees
"completed", reruns it, and the rerun rejoins the per-PR concurrency group
and cancels the new head's run. The PR is left testing a stale commit (seen
on #131236 and #131246). Skip the rerun when the head moved: the new run's
own event already carries the label. Same guard on the run-e2e job.
The memory-provider setup UI labelled a provider's Python dependencies from the manifest's
`pip_dependencies` / `python_dependencies` only. PM installs from an authored `pyproject.toml`
when one exists (it wins over the manifest lists), so a provider that declares its requirements
there showed an empty list, or a stale one, while the install button prepared different packages.
Read the same `read_python_declaration(...).install_requirements` PM consumes, so the label
matches what would be installed, including PM's own filtering (hermes-agent self-dependency,
direct URLs, `python_runtime: external`).
Both seams are public contracts for plugin/theme authors (first consumer:
t3-code-theme, #129600); the page documents the arbitration, failure
isolation and lifetime guarantees so a theme never needs DOM reads.
Theme CSS had no supported way to style a chat switch: t3-code-theme's
installSwitchFade watched the focus store, polled transcript rows and
scroll position in a rAF loop and toggled its own root attribute — DOM
reads catalog rule 8 now refuses.
Core already knows both phases of a switch, so it publishes them: the
chat surface root ([data-chat-surface], per pane) carries
data-session-switching="true" while a routed session is loading
(Thread loading === 'session') and while the thread list's scroll
restore places the new rows, removed on settle, user input, failed
resume or unmount. Each phase holds a token; the attribute goes when the
last holder releases, so the load ending never clears the restore's
mark mid-switch. An attribute (not a view-transition or motion token)
because the need is "know when", the duration is core's real settle
time, and CSS composes it with any theme without JS.
Plugins could relabel the model pill (COMPOSER_AREAS.modelPill) but the
menu it opens was core-only, so a theme wanting provider marks next to
each model (t3-code-theme, catalog #129600) had to MutationObserve the
open menu and paint into its rows, which catalog rule 8 now refuses.
A `modelMenu.row` data contribution supplies decorate({ provider, model,
label }) -> { icon?, badge? } | null. ModelFamilyRow paints the answer in
two fixed slots it owns: a leading 1rem icon box before the name and a
trailing chip after core's own chips. Per slot the first usable answer in
registry order wins, so an icon plugin and a badge plugin compose; a
throwing decorator or a non-element/non-string value declines, and the
icon renders inside its own error boundary so a crashing plugin component
blanks only its slot, never the menu. Applies to every ModelCatalogMenu
host (composer pill, kanban override), since a mark belongs to the model.
/fast now asks the same question the next turn asks: resolve_fast_mode_overrides
over the route _resolve_session_agent_runtime returns (session /model override,
channel override, persisted override after a restart), with provider and
base_url. Both the Priority gate and the Ultrafast gate added in #128307 read
that one route, so GPT-6 Astra picked with session /model on openai-codex gets
/fast and /fast ultrafast, and a fast-capable default no longer admits a
session route whose requests never carry the tier (gpt-5.4 via OpenRouter).
The tier override is also stored under the topic-recovered session key, the
one the next turn reads (#30479 class; /model and /reasoning already do this),
so on Telegram DM topics a session /fast is no longer dropped.
Route resolution runs off the event loop (it can refresh an OAuth token, as
the turn does); with no usable route at all the configured model is judged
alone, as before.
Tests: the four salvaged cases collapse into two invariants on the real
fast-mode tables (both gate directions + the turn's request_overrides, and
the recovered-topic key); red on origin/main.
Route /fast eligibility through _resolve_session_agent_runtime (the same
resolver normal turns use) instead of the global _resolve_gateway_model,
so a session-scoped /model override or channel override keeps /fast in
sync with the model the next agent turn actually routes to. Normalize the
command source exactly like /model does so the override is read under the
key it was stored, and let the resolver rehydrate persisted overrides a
gateway restart cleared from memory.
Rebased onto current main (was DIRTY): the lazy import in
_handle_fast_command now pulls only _load_gateway_config; the sibling
picker tests in test_choice_picker.py::TestFastChoicePicker are updated to
stub _resolve_runtime_agent_kwargs (matching test_fast_command.py) so the
no-override path doesn't hit real provider resolution in CI.
Tests: tests/gateway/test_fast_command.py (10) +
tests/gateway/test_choice_picker.py::TestFastChoicePicker (4) pass.
(cherry picked from commit 9885267a6f6e124b470fa2f597ef9c3623063549)
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.
The completed-tree shortcut treated "stamp names HEAD" as "nothing is
owed". But `hermes update` at an unchanged commit ("Already up to date")
re-arms the completion marker before running the tail; when that tail
fails or is killed, the old stamp still names HEAD and the next launch
silently cleared the marker instead of retrying it.
The stamp only proves the tail that wrote it. A marker is now discharged
only when it predates the stamp (the #123314 leftover from a previous
install); one armed after it is retried as before. The shortcut also
requires a clean tree on both sides: a stamp recorded dirty, or local
edits since, are no proof that the built products match the checkout.
Reported by @unsupportedpastels in review.
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.
Boot-time adoption of an unstamped blessed checkout publishes the same
identity a finished install/update does (commit == HEAD) but builds
nothing. Read as completion evidence, the launch that adopted (and every
later one) cleared a pending source-completion marker whose tail never
ran, and nothing on disk recorded the debt afterwards.
Adoption now stamps `adoptedAt`; the completed-tree check ignores such
stamps, and the next real completion rewrites the stamp without it. The
check also drops its blanket try/except: both reads it wraps never raise.
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>
A fresh install records its tree in install-stamp.json; a first launch under a preserved or pristine home then re-entered the completion tail against the same SHA (stale pending marker, or no per-home facts). When the stamp commit is HEAD the products were already built for this tree: provision a stale venv without arming, discharge leftover markers, and run the agent. Fixes#123314.
`new URL("//", origin)` threw inside the http request handler, so a
request like `GET //` escaped as an uncaught main-process TypeError and
was never answered. Reject unparseable targets up front.
`hermes status --full` printed two lists for the same platforms: a legacy
table judged by one env var, then "(plugin)" rows judged by the gateway.
Slack configured in config.yaml showed "✗ not configured" and
"✓ configured (plugin)" in the same section, and the legacy verdict could
disagree with the summary's Platforms line.
Render one list from the gateway config instead: registry entries bring
their labels, a small label table covers built-in adapters without one,
"configured" is get_connected_platforms() (the summary's verdict), and the
home channel comes from the loaded config, which also fixes the legacy
table reading QQ_HOME_CHANNEL where the gateway reads QQBOT_HOME_CHANNEL.
Dedupe idea from #54131; root-cause analysis of the check_fn verdict from
#96200.
Co-authored-by: igparkernz-coder <297591502+igparkernz-coder@users.noreply.github.com>
Co-authored-by: liuhao1024 <sunsky.lau@gmail.com>
On Desktop `default` and /skin default mean Nous (a CLI/TUI or config switch
to `default` moves a Desktop pick back to Nous), the gold look is the
separate `classic` theme reached by /skin classic, gold or hermes, and
`classic` joins the Desktop built-in names a user skin file cannot recolor.
A Desktop that ran the reverted #130015 build has the CLI `default` skin
cached under `hermes-desktop-backend-themes-v1`, labelled "Classic Hermes".
readCached only dropped built-in names, so that entry survived next to the
new `classic` theme: two Classic cards, the stale one previewing gold and
applying Nous, and two Classic rows in /skin list. readCached now also drops
every retired name (`default`, `gold`, `nous-light`, moved to presets.ts as
RETIRED_SKINS so the store and the theme provider share it) and writes the
cleaned map back, so the entry is gone from disk on the first launch. The
same rule keeps ingestBackendSkin from registering a theme under a retired
name.
Classic also ignored light mode: it took the single-mode converter output,
dark palette in both slots. Its light variant now comes from the CLI
default skin's `light_colors` ladder (#F5F5F5 surface, goldenrod accent)
with a deeper ink of the same brown, so Desktop's 74% / 54% secondary and
tertiary text land at Nous Light's contrast (6.5:1 / 3.5:1; body 12.3:1).
Dark is unchanged. The description now reads "Gold on navy, the CLI's
original look" and the `as DesktopTheme` cast is gone.
Desktop users can now choose the CLI's classic gold-on-navy look in
Settings > Appearance (and via /skin classic, gold or hermes).
It ships as a Desktop built-in under its own id `classic`, converted from
the CLI `default` skin colors through the same skinToDesktopTheme path a
backend skin takes. It is deliberately NOT registered under `default`:
stock config ships `display.skin: default`, and backend sync / the boot
fallback adopt that value, so un-retiring `default` (the reverted #130015)
repainted every user who never picked a theme in Classic gold and cached
it. `default` stays retired and aliased to Nous; Classic is only painted
when someone picks it. Desktop never writes its pick to display.skin, so
the pick stays Desktop-local like every other Desktop theme.
Re-lands #130015 (reverted in #130852).
Co-authored-by: 686f6c61 <github@00b.tech>
Call renderActionItem instead of declaring an Item component inside
shellSections, which minted a new component type on every render and
remounted the menu rows. Restore the tree-group file doc comment above
its imports.
Keep explicit nested menus and bare pane operations while restoring text, editable, link, and image menus. Reuse the app actions in a fallback section of the pane-body menu.