12896 Commits
Author SHA1 Message Date
Maciej Lisiewski df9ffa977d fix(desktop): rebuild a source build whose linked Electron is gone
A Windows ZIP update drops workspace node_modules. If the reinstall then
failed, the renderer and its receipt stayed current, so
`hermes desktop --source` skipped the build and could not launch.
Treat a missing apps/desktop/node_modules/electron as a needed build.
2026-11-06 04:08:46 +01:00
Maciej Lisiewski b115764185 fix(terminal): keep the user's own NPM_CONFIG_* in terminal shells
Package managers inject lowercase npm_config_*/npm_package_*/pnpm_config_*
names; the case-insensitive strip also removed a user's uppercase
NPM_CONFIG_REGISTRY or NPM_CONFIG_USERCONFIG, breaking a configured mirror
in the integrated terminal. Strip the injected spelling only, as main did.
2026-11-06 04:08:46 +01:00
Maciej Lisiewski 961ebe31f8 fix(source-build): bridge ~/.npmrc when no userconfig is set
npm reads ~/.npmrc by default and exported its keys to install scripts as
npm_config_*; pnpm 12 does neither, and the bridge only read an explicit
NPM_CONFIG_USERCONFIG or $HERMES_HOME/npmrc. Mirror keys kept in ~/.npmrc
(disturl, node-pre-gyp binary hosts) stopped reaching node-gyp builds.
Fall back to ~/.npmrc, as npm does.
2026-11-06 04:08:46 +01:00
Maciej Lisiewski 903139503d fix(source-build): bridge the npmrc npm would read, with values decoded
The npm_config_* bridge for dependency install scripts always read
$HERMES_HOME/npmrc, even when NPM_CONFIG_USERCONFIG selected another file:
its registry then overrode the user's file, and settings only in the
selected file (native-download mirrors) never reached the scripts. It also
exported quoted values with their quotes, so `registry="https://..."`
became a malformed URL that broke every install. Read the file npm itself
would (an explicit userconfig wins) and strip surrounding quotes as npm does.
2026-11-06 04:08:45 +01:00
Maciej Lisiewski ced0ea775d fix(terminal): keep package-manager settings out of user shells
A desktop or dashboard started through `pnpm run` carries pnpm_config_*
(store, cache and state dirs) besides npm_config_*/npm_package_*, and the
user's own pnpm in the integrated terminal would pick them up. Both shell
builders, Electron's terminal IPC and the dashboard host terminal, now drop
all three prefixes, case-insensitively (Windows env names are).
2026-11-06 04:08:24 +01:00
Maciej Lisiewski 7062229090 chore(js): switch the workspaces from npm to pnpm
pnpm 12.15.0 (native executable) installs every JS workspace from one
pnpm-lock.yaml, imported from package-lock.json with the same resolved
versions. website/, the WhatsApp bridge and the photon sidecar stay separate
npm projects.

Configuration moves from .npmrc and the root manifest into
pnpm-workspace.yaml: the 14-day minimumReleaseAge and its excludes,
overrides, allowBuilds (the old allowScripts set, with strictDepBuilds
failing any unlisted build script), engineStrict, sideEffectsCache: false
(in-place rebuilds must not write into the store) and
verifyDepsBeforeRun: false (a `pnpm run` never installs).

The pnpm version is pinned in pm/lock.json and in root devEngines.
pmOnFail: ignore keeps that pin out of pnpm-lock.yaml: a leading pin
document makes osv-scanner read zero packages. scripts/build/node-deps.mjs
enforces the exact version instead, and every lock reader refuses a
multi-document lock. A root preinstall refuses `npm install`; devEngines
onFail is warn so npx (MCP servers, the tsc linter) keeps working inside
the checkout.

User-machine installs run --frozen-lockfile --trust-lockfile (release age is
checked when locking, in CI, not per user). node-deps removes npm-era
node_modules trees before the first pnpm install. Nix builds the store with
fetchPnpmDeps and a hash file that CI checks.
2026-11-06 04:08:24 +01:00
unsupportedpastels 9242ce464b fix(dashboard-auth): /api/status on the Webapp reads the session-token header as signed in
The gate now accepts the deployment session token in X-Hermes-Session-Token on
the gateway's Webapp, but peek_session (used only by /api/status to decide what
a signed-out Webapp caller sees) still read just the bearer and the cookies. So
a Desktop "Session token" connection was signed in on gated routes and got the
signed-out /api/status body, without install_id or gateway_shared_with.

peek_session now tries the header in the same place the gate does: no bearer
and neither session cookie present. It goes through the same helper, so only
the session-token provider verifies the value and the Webapp bearer rule
applies; peek stays side-effect free (no refresh, cookie writes, audit or 401).
Bearer, cookie and no-credential requests read as before.

Docs: the Desktop guide lists the header among the ways the deployment token
works on the Webapp.

Patch from @BearHuddleston's review.
2026-11-05 19:56:12 -05:00
unsupportedpastels b5c92b42dd fix(dashboard-auth): the session-token header follows the Webapp bearer rule
The X-Hermes-Session-Token check now uses webapp_sign_in.access_token_works_alone,
the same rule the bearer, peek and WS paths use, so on the gateway's gated Webapp
the deployment session token works in the header as it does as a bearer. Only the
session-token provider still verifies the header.
2026-11-05 19:56:12 -05:00
unsupportedpastels 9bf07bf5c7 fix(dashboard-auth): accept the Desktop's session-token header behind the gate
Hermes Desktop's "Session token" connection sends the deployment session
token in the X-Hermes-Session-Token header on REST. The gated middleware
read only a bearer token from the Authorization header, so every REST
call from such a connection got 401 no_cookie even with the right token.

When no bearer is sent and neither the access nor the refresh session
cookie is present, the gate now verifies that header with the registered
session-token provider only. A request carrying a session cookie takes
the cookie path exactly as before (the header is not consulted). Any
other header value, or no registered provider
(dashboard.session_token_auth.enabled: false), also leaves the request
to the cookie path. A bearer still wins, and on the gateway's Webapp the
header follows the same token_works_alone rule as a bearer. The gate's
two cookie-write 403 checks move, unchanged, into _cookie_write_refusal
so the gate stays under the complexity cap.

Refs #147387
2026-11-05 19:56:12 -05:00
unsupportedpastels 323c318c4e fix(plugins): refuse to turn on a recalled plugin from the plugins picker
The bare `hermes plugins` picker saved ticked rows straight through
admission, so a plugin the catalog recalled after it was installed could
still be switched on there (the load gate then refused to import it, so
it never ran). `plugins enable` and the dashboard toggle already refuse it.

The picker now checks each newly ticked row against the kill list before
the atomic save. On a refusal both the curses screen and the text
fallback print "Plugin selection refused, not saved" and write nothing.
Plugins installed with --allow-removed and non-recalled plugins are
handled as before.

Patch from BearHuddleston's review on #148776.
2026-11-05 19:16:24 -05:00
unsupportedpastels 57cc40f469 refactor(plugins): dashboard plugin APIs import through the shared user-code helper
_mount_one_plugin_api loads a plugin's dashboard API file with
plugin_isolation.import_user_code instead of its own copy of the
spec -> sys.modules -> exec -> pop-on-failure steps. Same module name
(hermes_dashboard_plugin_<name>), same host-mode routing before the import,
and a file with no module spec is still skipped silently.

Tests check behavior on both sides of the change: host-isolated user APIs
go to the plugin host without running in-process, in-process APIs execute
registered in sys.modules and are removed on failure, and a non-Python api
file mounts nothing and logs nothing.
2026-11-05 18:58:58 -05:00
unsupportedpastels 0502fe0597 feat(plugins): one published list of what plugins may import
The public list `hermes plugins validate` checks plugin imports against
was a hardcoded frozenset inside hermes_cli/plugin_validate_private_imports.py,
with no page that showed it to plugin authors.

Move the list, unchanged (89 entries), into hermes_cli/data/plugin_api.json
(already shipped as package data via `data/*.json`) and load it once in the
validator. scripts/gen_plugin_api_docs.py renders
website/docs/developer-guide/plugins/public-api.md from the same file, and a
test fails when the page is stale. Validate output is unchanged for the same
inputs.
2026-11-05 18:47:10 -05:00
unsupportedpastels 7e8c8a5856 fix(plugins): keep a reloaded hosted plugin when an abandoned load fails late
A hosted load abandoned at its load deadline can fail after a reload of
the same plugin succeeded. Its failure cleanup unloaded the plugin key,
which tore down the live load: its tools failed with "plugin host
callable was released" and its on_unload had already run.

Claim the plugin key before the host start can block, and run the
failure cleanup only while this load still owns the key.
2026-11-05 18:29:48 -05:00
unsupportedpastels f23fb505e7 fix(plugins): keep a failed hosted load's error when its cleanup fails
The failed-load cleanup added in the previous commit could raise before
the original exception was re-raised, so the plugin's recorded error
became the cleanup's (an on_unload that raised SystemExit, or one that
exited the host). Run the cleanup best-effort, log its failure, and
re-raise the register() error.
2026-11-05 18:29:48 -05:00
unsupportedpastels 994e901730 fix(plugins): run a failed hosted load's on_unload cleanup
Under plugins.isolation: host, a plugin whose register() queued an
ctx.on_unload callback and then raised kept that callback (and any
objects it handed over) in the plugin host. The in-process loader runs
the callback when register() fails, so a hosted plugin that started a
thread or opened a resource before failing leaked it until the host
exited, and a later successful load inherited the stale callback.

PluginHost.load now unloads the plugin key in the host when the load
fails, which runs the queued callbacks and drops the host-side
references, matching the in-process failure path.
2026-11-05 18:29:48 -05:00
unsupportedpastels 91e7b43bad fix(plugins): plugin packs refuse a bare name whose catalog entry is recalled
A bare pack name resolves to the catalog entry's repo identifier before the
install core runs, so a name-only recall of an entry whose manifest name
differs matched nothing and the pack published and enabled it. The pack
now checks the catalog name itself, as `hermes plugins install <name>`
already does.
2026-11-05 18:28:39 -05:00
unsupportedpastels f77f25a350 fix(plugins): judge enable of a nested plugin by its own manifest and directory
The enable kill-list check re-resolved the plugin directory from the bare
leaf of its key, so plugins/obs/x beside a top-level plugins/x read the
other plugin's install record: a clean nested plugin could be refused, or
enabled on the other plugin's allow_removed exemption. Dashboard/TUI and
CLI enable now pass the resolved entry's manifest name and directory, the
same inputs the load-time gate uses.
2026-11-05 18:28:39 -05:00
unsupportedpastels 347833a129 fix(plugins): updates of --allow-removed subdirectory installs keep working
A subdirectory plugin install has no .git, so `hermes plugins update` and the
dashboard/TUI update re-clone it through _install_plugin_core. That core now
checks the kill list, but the re-clone did not pass on the install record's
saved allow_removed exemption, so updating a recalled plugin the user had
installed with --allow-removed was refused.

_pull_plugin_update now forwards the record's allow_removed into
_reclone_plugin_update and on to the core, which writes it back on the new
record. Installs without the exemption are still refused before any re-clone.
2026-11-05 18:28:39 -05:00
unsupportedpastels ca17481680 fix(plugins): plugin packs and dashboard/TUI enable respect the plugin kill list
The kill list (removed.yaml) was checked by each caller. Plugin packs
installed and enabled recalled plugins, and the dashboard/TUI enable
path skipped the check the CLI enable command runs, so a recalled
plugin's Python dependencies could reach the shared venv before the
load-time gate refused it.

The check now runs inside the shared cores every surface uses:

- _install_plugin_core refuses a kill-list match by source, git URL,
  manifest name or catalog name once the manifest is read and before
  the scan or publication, unless allow_removed is set.
- _set_plugin_enabled refuses enabling an installed non-bundled plugin
  on the kill list (an install record carrying allow_removed stays
  exempt). Disable is unchanged.

Callers that caught only AdmissionRefused around an enable (dashboard
and TUI toggle, plugin packs, the install-card enable) now also turn
the refusal into their normal error result. The CLI pre-checks and
messages are unchanged; the load-time gate stays as a backstop.

Code Health PMF-03, plan step A1.6.
2026-11-05 18:28:39 -05:00
unsupportedpastels 0cbe4e5a51 fix(plugins): a reload publishes its own staged context reference
The staged context references were found by slicing the manager's
registration list from the offset saved at load start. An earlier
registration disposed while the plugin loaded shortens that list, so the
reload's entries slid below the offset and the old provider kept answering
while the plugin was reported as reloaded.

Each load's PluginContext now keeps the registrations it staged, and a
successful load publishes exactly those, then clears the list so a
provider that holds its ctx does not keep the provider it replaced.
2026-11-05 18:27:13 -05:00
unsupportedpastels 7ba15cc975 fix(plugins): a plugin reload replaces its context reference once it loads
A forced plugin re-discovery (reload) re-runs register(), but the plugin's
@-prefix was still held from its first load, so the new provider was refused
with "prefix 'x' is already registered" and the stale one kept answering.

Context-reference registrations now go through the plugin ownership ledger.
The prefix table remembers which plugin on which manager claimed a prefix.
Unloading keeps the provider registered, as before, and only releases the
claim. When the same plugin on the same manager registers that prefix again,
the new provider is staged on a ledger entry and published only after the
plugin's load succeeds. A load that fails or times out disposes the entry
first, so the working provider and its owner stay exactly as they were.

Everything else matches main: providers stay in one process-wide table shared
by every profile; a second registration of a prefix in the same load, another
plugin, another profile's manager or a second manager are refused with the
same warning; built-in prefixes are refused; both registration calls still
return None when called as before.
2026-11-05 18:27:13 -05:00
unsupportedpastels 01888fc206 fix(webapp): the gated Webapp accepts the deployment session token
On the gateway's Webapp behind the sign-in gate, a provider token worked
on its own (bearer header, socket ?token=) only when this listener gave
it to a native app's sign-in. The deployment session token
(HERMES_DASHBOARD_SESSION_TOKEN, literal or file:<path>) that the bundled
session-token provider vouches for was therefore refused with 401 /
close 4401, although the docs promise it authorizes the gated listener.

The bearer, peek_session and socket ?token= checks now call
access_token_works_alone: token_works_alone (looked up at call time) or
the token the registered session-token provider read at startup,
compared in constant time by the provider's matches_token, which its
verify_session also uses. With the provider not registered
(dashboard.session_token_auth enabled: false) the token is refused as on
main. token_works_alone and the refresh route are unchanged from main:
the deployment token has no refresh.
2026-11-05 18:26:06 -05:00
unsupportedpastels d6fcfe57d9 docs(plugins): import_user_code describes what it is today
It is shared import steps for loaders that run a single file of plugin
code in-process, not yet the only such place: other loaders still run
their own spec/sys.modules/exec blocks.
2026-11-05 18:25:50 -05:00
unsupportedpastels 81ab7c7cf3 refactor(plugins): gateway hook loader imports handler.py through one shared helper
Add hermes_cli.plugin_isolation.import_user_code(path, module_name): the
spec -> sys.modules -> exec -> pop-on-failure steps for loading one file of
user code, returning None when the file has no module spec. The gateway
HOOK.yaml loader now uses it instead of its own copy of those steps.

No behavior change: hooks load exactly as before under every
plugins.isolation mode, the 'could not load handler.py' skip line and the
'Error loading hook' path are unchanged. Keeping user hooks out of the
gateway process under host isolation is a separate, opt-in change.
2026-11-05 18:25:50 -05:00
unsupportedpastels a2db0efb29 test(cli): pin why get_fallback_chain stays importable from oneshot
Replace the symbol-absence test with one that pins the kept re-export,
and spell out in the comment that the -z run does not read it.
2026-11-05 17:59:21 -05:00
unsupportedpastels d52c04b7d7 fix(cli): keep get_fallback_chain importable from hermes_cli.oneshot
The runner that used it is gone, but the name was importable from this
module on main and external callers patch it there; removing it turns
their patch into an AttributeError.
2026-11-05 17:59:21 -05:00
unsupportedpastels 936f3465f8 refactor(cli): remove the unused one-shot agent that ran outside the gateway
`hermes -z` has run through the gateway (`_run_oneshot_from_args` ->
`launch_gateway_chat`) at every dispatch site for a while, but the old
in-process runner stayed behind: `hermes_cli.oneshot.run_oneshot` /
`_run_agent` built a bare AIAgent outside the session authority, and
`hermes_cli.main._run_and_exit_oneshot` (with its
`_cleanup_oneshot_runtime` cleanup table) was its only caller. Nothing
outside tests reached any of it.

Delete that path. `hermes_cli.main._exit_after_oneshot` stays exactly as
it is: `--cpu-profile` names it as the one-shot hard-exit path and it
writes the profile before `os._exit`; only its docstring loses the
pointer to the removed cleanup helper. `hermes_cli/oneshot.py` keeps
only what the gateway path and gateway CLI turns still import: the
exit-code mapping, the `--usage-file` ledger with its auxiliary-usage
delta, and `--toolsets` normalization. The process-receipt eval probe
now calls `process_registry.wait_for_pending_completions` directly.

Tests that only exercised the deleted runner are removed; the exit-code
and usage-file tests now call the kept helpers directly; the cost-guard
test patches the gateway launcher instead of the removed function. A new
test pins that `hermes -z` dispatches to `launch_gateway_chat`.

Refs #145171 (EP-11a)
2026-11-05 17:59:21 -05:00
unsupportedpastels 70147917c1 fix(gateway): a busy gateway can't be retired through the dashboard idle route
The gateway mounts the dashboard app on its own listener. There the idle
proof read only a standalone serve child's ledgers, never the gateway
runner, so with an agent running GET /api/health/idle answered idle and
POST /api/health/retirement prepare/commit took the retirement fence.
Afterwards dashboard RPCs, cron ticks and new admissions were refused
until restart while the gateway kept running.

When the app is mounted on a gateway (app.state.gateway_runner set), the
idle route now reports idle false with reason gateway_owns_lifecycle and
the retirement route refuses every action with the same reason, without
touching the fence. The gateway keeps retiring through its own idle exit.
Standalone serve behavior is unchanged.

Refs #145171 (EP-15)
2026-11-05 17:44:06 -05:00
unsupportedpastels 27f4a045da fix(delegation): route saves tell a null extra key from an omitted one
_clean_model_option read extra route keys with entry.get(), so an
explicit null and an omitted key looked the same: a declared key sent
as null was dropped, and a stored extra key holding null survived a
save that left it out (the editor's way of turning a setting off).
Both loops now decide on whether the key is present: null is kept as a
value, and an omitted key is removed.
2026-11-05 15:44:58 -05:00
unsupportedpastels 494b618eb9 feat(delegation): backends declare their own route keys; route saves keep them
A delegation backend's routes can carry keys of its own (e.g. a
per-route `daybreak` opt-in), but PUT /api/delegation/backends/model_map
copied only core's route fields, so any Desktop save silently dropped
them and the route kept running without the setting.

- DelegationProvider.route_keys (default empty): keys of the backend's
  own route entries that settings editors may write and must keep.
- BackendInfo / GET /api/delegation/routing list each backend's
  declared route_keys (blank, non-string and repeated entries dropped;
  a broken plugin property declares nothing instead of hiding the
  backend).
- The model_map save keeps declared keys as sent (the backend validates
  them) and any other key the stored route already carries when the save
  sends it back unchanged; a key nobody stored is still dropped.
2026-11-05 15:44:58 -05:00
IAvecilla 1e81ee57e3 fix(webapp): voice relays through the host, and a created profile is served right away 2026-11-05 10:15:02 -08:00
kshitijk4poor c22f5f8483 fix(plugin-host): keep a remote RuntimeError subclass's class name
A RuntimeError subclass raised in the plugin host (a transport or credential error)
stays a PluginHostError with its type_name instead of arriving as a bare RuntimeError:
PluginHostError already is a RuntimeError, so mapping the base gained callers nothing and
lost the class name. A subclass of another mapped builtin still comes back as that base,
now with its class name kept in the message.

Tests: the RuntimeError-subclass round trip, and the memory panel GET returning 400 when
the provider cannot read its config.
2026-11-05 13:08:14 +05:30
kshitijk4poor ddf69149ed fix(memory): keep refused saves a 400 across the plugin host; hooks survive SystemExit
- plugin_host_wire: an error crossing the host now carries the nearest builtin base
  class, so a ValueError subclass (json.JSONDecodeError) raised by a hosted provider's
  write_config_fields is still a ValueError in core and the dashboard returns 400, not 500.
- notify_memory_provider catches SystemExit as well as Exception: a provider hook that
  exits no longer skips rename's alias, active-profile and identity updates.
- Docs and docstring: a key missing from the target profile's .env reads as unset; the
  hooks never fall back to the caller's environment.
- Memory panel GET maps a provider read failure like PUT does (ValueError -> 400).
- One helper for submitted secrets; the Honcho placeholder rule moves into its storage
  branch so the field loop no longer branches on storage kind.
- Tests: wire round-trip of error classes, SystemExit hook, missing-secret fail-closed,
  and the failing-hook test now asserts both hooks ran.
2026-11-05 13:08:14 +05:30
kshitijk4poor e090092285 feat(memory): profile lifecycle and config-storage seams for memory providers
A catalog memory provider (Honcho today) has no way to own its per-profile setup or its
dashboard config file, so core carries that code for it: profile create/rename edit Honcho's
host blocks and the dashboard has a Honcho-only storage kind that imports the plugin's private
modules. These seams let a provider do that work itself; core's Honcho code is untouched here
and is removed in a follow-up once the plugin implements them.

- MemoryProvider.on_profile_created(profile_name, hermes_home) / on_profile_renamed(old, new,
  hermes_home): called from create_profile (CLI, dashboard, Desktop/TUI) and rename_profile for
  the provider the profile's effective config selects, with that profile's home and its own
  .env secrets bound. A failing hook or unreadable config is logged and never fails the create
  or rename. No-ops by default.
- storage=STORAGE_PROVIDER in config_schema.py, with read_config_fields(hermes_home) /
  write_config_fields(values, hermes_home): the provider owns its file layout. Core keeps
  value coercion, placeholders, write-only secrets, the memory.provider switch and the 400 on
  a refused write, and saves secrets to .env only after the provider accepted them.

_record_profile_rename moves to profile_identity.py (profiles.py line budget).
2026-11-05 13:08:14 +05:30
Teknium 1a71782f17 feat(cli): backtrack picker — Esc Esc / /rewind to edit & resend or fork from an earlier prompt
Classic CLI gains a Codex-style backtrack picker. Esc Esc on an idle, empty composer with no
overlay (or /rewind) lists earlier prompts newest first with a preview and turn number.

- Enter: prefill the prompt for editing. Nothing is cut until the edit is submitted; the
  submission rides to the process loop as a BacktrackResend, which re-validates the picked
  turn (same session, same text) and then runs the same durable rewind /undo uses. Esc Esc,
  Ctrl+C, an empty submit, or submitting a slash/! command cancels and cuts nothing.
- f: fork. Runs the existing /branch path, then rewinds only the CHILD to just before the
  picked prompt and prefills it; the parent's rows stay active.
- /rewind N and /rewind fork N do the same without the picker.

Esc Esc with a draft still discards it; mid-turn Esc Esc is unchanged.

undo_last's rewind body moves into CLISessionMixin._rewind_to_user_ordinal (shared by /undo
and the picker; same compaction-carrier handling, agent patching and memory rewound hook).
The picker lives in a new hermes_cli/cli_backtrack.py mixin; cli_tui_mixin.py is untouched.
CommandDef rewind is cli_only (desktop: terminal); desktop registry and parity table regenerated.
2026-11-04 23:06:14 -08:00
Teknium a6efd813d3 fix(plugins): unattended updates re-check the hold and the widened surface at the mutation boundary
- 2.1 hold recorded during the check: update_one/dashboard_update_user_plugin(unattended=True) re-read
  the install record under the per-plugin update lock and skip a --ref hold (HeldWithRef / held);
  _install_plugin_core takes the same lock, so an install --ref cannot publish between the re-check
  and the updater's publication.
- 2.2 custom Git widening: the unattended gate compares the staged candidate's surface (tools, hooks,
  capabilities, ...) with the installed one before publication (git pull and subdirectory re-clone)
  and returns needs_review; a named update still applies it.
- /plugins update <name|all|--yes> refuses extra arguments with a usage line instead of updating
  every plugin.
- hermes plugins update (cmd_update_all) skips catalog installs held with --ref, like every other
  Update all.
2026-11-04 21:58:12 -08:00
Teknium d063eefc67 fix(plugins): review nits — caution wording, reload wording, per-profile Update all
- Docs/docstrings: an unattended update never accepts a caution verdict on a git update;
  a catalog re-pin accepts one (a reviewer read that pin at admission). Name the --ref hold.
- /plugins update, Desktop and the dashboard reload each updated plugin (MCP + skills live now,
  Python tools in new sessions); the scheduled check's 'loads in new sessions' stays.
- Desktop: $pluginUpdateAllRunning is a per-profile set, so profile A's run leaves B's button enabled.
2026-11-04 21:58:12 -08:00
Teknium d15647cb0a fix(plugins): a widened pin on the auto-apply tick logs one review line, not a traceback
The tick's default apply is now update_one with no terminal (plugins_auto_update.apply_unattended),
so refusals reach the tick as exceptions instead of cmd_update's SystemExit. RepinConsentRequired
logs one INFO line naming `hermes plugins update X`; other refusals log one line.
2026-11-04 21:58:12 -08:00
Teknium 1089d07950 fix(tui): run /plugins update over plugins.manage update_all, not the slash worker
The slash worker is killed after 45 s, which could cut a clone/scan/PM sync mid-update.
The TUI now calls the same update_all RPC Desktop uses (600 s budget) and renders its rows.
2026-11-04 21:58:12 -08:00
Teknium 8a45d36216 fix(plugins): unattended updates leave a catalog install held with --ref alone
A catalog plugin installed with --ref (pinned, empty catalog.pin) is a deliberate hold
(a rollback). The auto-apply tick, update_all_unattended and catalog_pins_behind now skip
it and report it as held with --ref; an explicit `hermes plugins update <name>` still moves it.
2026-11-04 21:58:12 -08:00
Teknium 2e933c501e feat(dashboard): Update all and Auto-update on the Plugins page
POST /api/dashboard/agent-plugins/update-all runs the same unattended
update-all as Desktop and /plugins update (nothing accepted on the
user's behalf; widened pins come back needs_review), and
GET/PUT /api/dashboard/plugins/auto-update read and set the profile's
plugins.auto_apply. Both run in the dashboard profile scope off the
event loop. The Plugins page gets the button and the switch; copy in
every dashboard locale. Plugin catalog types move to api-plugins.ts to
keep api.ts under its size cap.
2026-11-04 21:58:12 -08:00
Teknium 5819226468 feat(plugins): one switch and one action for unattended plugin updates
main already ships `hermes plugins update` (no name = update all) and the
opt-in `plugins.auto_apply` cadence. This makes both reachable from every
surface and closes the gaps reported on the original PR:

- Catalog installs now ride `plugins.auto_apply` too (it moved only git
  rows, so catalog plugins stayed on their install-day pin). The update
  path still refuses a widened pin or a too-new requires_hermes unattended.
- One updater per plugin: a cross-process lock around update_one and the
  dashboard/Desktop update, so the gateway's auto-apply tick and a manual
  update never run on the same plugin at once; the loser changes nothing.
- `hermes update` names, per profile, the catalog installs its new catalog
  pins left behind and re-arms that profile's plugin check, so an opted-in
  gateway applies them on the next tick.
- New CLI verb `hermes plugins auto-update [on|off]`; `/plugins update` and
  `/plugins auto-update [on|off]` in the classic CLI and the TUI (slash
  worker).
- RPC `plugins.manage` actions `update_all` and `auto_update` (contract +
  regenerated apps/shared outputs) for Desktop.

Off by default (legacy behaviour); all writes are profile-scoped through
get_hermes_home()/the scoped RPC.
2026-11-04 21:58:12 -08:00
Teknium c58e6de692 docs(delegation): prefix_stagger_seconds is a cap on the warm-up wait, not a stagger 2026-11-04 21:50:30 -08:00
Teknium 8226ce6462 chore(delegation): keep config_defaults at its line cap; event-driven gate test 2026-11-04 21:50:30 -08:00
Teknium 6d0686ba3c feat(delegation): opt-in prompt-cache warm-up gate for batch subagents
Sibling subagents in one tasks batch send byte-identical tools[] and system
prompt scaffold; only the goal (their first user turn) differs, plus the system
prompt tail when tasks carry different context. Started together, every
child's first request misses the provider prompt cache and pays the full prefix.

delegation.prefix_stagger_seconds (default 0 = off) now holds the siblings on
the first child's route until that child's first response starts streaming
(its request has written the cache), it finishes, or the cap elapses (max 60 s).
The wait ends as soon as the cache is readable, so opting in costs about one
time-to-first-token instead of a fixed (N-1)*S ramp. Routes Hermes does not
prompt-cache and siblings on a different model start without waiting, and a
sibling stopped during the wait is reported interrupted without starting.

Rebuilt on the split delegate_tool_* layout: the gate is its own sibling
(tools/delegate_tool_prefix_gate.py); dispatch only wraps run_child.
2026-11-04 21:50:30 -08:00
Teknium 79f28901db docs(gateway): adapter guides and plugin allowlist name resolve_gateway_approval_card
An outside adapter following the guide kept the answer-the-oldest bug, and one using the new
helper drew a private-import warning at admission. Review follow-up.
2026-11-04 21:40:32 -08:00
Teknium c10474fbb6 docs(stt): blank per-provider language inherits stt.language, not auto-detect
The per-provider `language: ""` comments in config_defaults.py,
cli-config.yaml.example and the voice-mode docs said blank means
auto-detect. Since bc997a36a8 made stt.language default to "en", a
blank provider value inherits that, so users following the comment got
English transcripts of non-English audio (#120100). Correct the text
to the real resolution order; no behaviour change.

Reported-by: Baophan00 (PR #120130)
2026-11-04 21:38:25 -08:00
Teknium 695d9a641d fix(cli): write the --cpu-profile before an in-place exec handoff
exec skips atexit, so relaunch() (/update, sessions browse -> resume) and the
named-profile dashboard reroute dropped the profile on POSIX. Both now write it
right before exec; the Windows branches already exit through atexit.

Review: "A process that leaves through exec drops its profile".
2026-11-04 21:32:52 -08:00
Teknium ce154453eb fix(cli): --cpu-profile starts where each launch path parses argv
--version and the deferred Termux classic-chat launch parsed --cpu-profile but
never reached _prepare_agent_startup, so no profile was written. Profiling now
starts right after each entry path parses argv (main, fast chat, Termux CLI,
Termux TUI), before it branches; the Termux interactive branch keeps its lazy
agent startup.

Review: "--version and the deferred Termux classic-chat path accept
--cpu-profile but never enable it".
2026-11-04 21:32:52 -08:00
Teknium 6b2895cacb fix(update): list --cpu-profile in the holder value-flag fallback
Review nit: keeps _HOLDER_VALUE_FLAGS_FALLBACK in step with the
parser fallback for trees where the parser cannot import.
2026-11-04 21:32:52 -08:00