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.
This commit is contained in:
Teknium committed 2026-11-04 21:58:12 -08:00
1 parent d15647cb0a
commit d063eefc67
7 files changed
+65 -16

No files matched your search

@@ -10,6 +10,7 @@ import { notify, notifyError } from '@/store/notifications'
import {
$pluginAutoUpdate,
$pluginUpdateAllRunning,
isPluginUpdateAllRunning,
loadPluginAutoUpdate,
setPluginAutoUpdate,
updateAllAgentPlugins
@@ -24,7 +25,7 @@ export function PluginUpdateAllActions({ scope }: { scope: null | string }) {
const p = t.skills.plugins
const { requestGateway } = useGatewayRequest()
const gatewayState = useStore($gatewayState)
const running = useStore($pluginUpdateAllRunning) !== undefined
const running = isPluginUpdateAllRunning(useStore($pluginUpdateAllRunning), scope)
const auto = useStore($pluginAutoUpdate)
const autoForScope = auto && auto.profile === scope ? auto : null
@@ -0,0 +1,21 @@
import { afterEach, describe, expect, it, vi } from 'vitest'
import { $pluginUpdateAllRunning, isPluginUpdateAllRunning, updateAllAgentPlugins } from './plugin-update-all'
describe('plugin update-all running state', () => {
afterEach(() => $pluginUpdateAllRunning.set(new Set()))
it('marks only the profile whose run is in flight', async () => {
let finish!: (value: unknown) => void
const request = vi.fn(() => new Promise(resolve => (finish = resolve))) as never
const run = updateAllAgentPlugins(request, 'work')
expect(isPluginUpdateAllRunning($pluginUpdateAllRunning.get(), 'work')).toBe(true)
expect(isPluginUpdateAllRunning($pluginUpdateAllRunning.get(), null)).toBe(false)
expect(isPluginUpdateAllRunning($pluginUpdateAllRunning.get(), 'home')).toBe(false)
finish({ results: [] })
await run
expect(isPluginUpdateAllRunning($pluginUpdateAllRunning.get(), 'work')).toBe(false)
})
})
+23 -4
View File
@@ -24,8 +24,27 @@ export interface PluginUpdateAllSummary {
updated: number
}
/** Profile whose update-all is running (null = none). */
export const $pluginUpdateAllRunning = atom<null | string | undefined>(undefined)
/** Profiles whose update-all is running (the default profile is `''`): a run for one profile leaves
* another profile's button enabled. */
export const $pluginUpdateAllRunning = atom<ReadonlySet<string>>(new Set())
const profileKey = (profile: null | string) => profile ?? ''
export const isPluginUpdateAllRunning = (running: ReadonlySet<string>, profile: null | string) =>
running.has(profileKey(profile))
function markRunning(profile: null | string, on: boolean) {
const next = new Set($pluginUpdateAllRunning.get())
if (on) {
next.add(profileKey(profile))
} else {
next.delete(profileKey(profile))
}
$pluginUpdateAllRunning.set(next)
}
export const $pluginAutoUpdate = atom<PluginAutoUpdateState | null>(null)
// One minute per plugin is the single-update budget; a whole sweep gets ten.
@@ -40,7 +59,7 @@ export async function updateAllAgentPlugins(
request: GatewayRequest,
profile: null | string
): Promise<PluginUpdateAllSummary> {
$pluginUpdateAllRunning.set(profile)
markRunning(profile, true)
try {
const result = await request<PluginsManageResult>(
@@ -62,7 +81,7 @@ export async function updateAllAgentPlugins(
failed: countOf(rows, 'failed')
}
} finally {
$pluginUpdateAllRunning.set(undefined)
markRunning(profile, false)
}
}
+3 -2
View File
@@ -112,7 +112,8 @@ def _clear_check_marker(home: Path) -> None:
def _apply_one(name: str) -> dict[str, str]:
"""One plan row through the dashboard's non-interactive update. A widened catalog pin or a
caution scan is NOT accepted on the user's behalf: the row comes back ``needs_review``."""
caution scan of a git update is NOT accepted on the user's behalf: the row comes back
``needs_review`` (a catalog re-pin accepts caution: a reviewer read that pin at admission)."""
from hermes_cli.plugins_cmd_update import dashboard_update_user_plugin
try:
res = dashboard_update_user_plugin(name)
@@ -164,7 +165,7 @@ def summary_lines(outcome: dict[str, Any]) -> list[str]:
if any(r["status"] == "needs_review" for r in outcome["results"]):
lines.append(" Review those with `hermes plugins update <name>` in a terminal.")
if any(r["status"] == "updated" for r in outcome["results"]):
lines.append(" Updated plugin code loads in new sessions.")
lines.append(" Updated plugins were reloaded: MCP servers and skills are live now, Python tools load in new sessions.")
return lines
+1 -1
View File
@@ -235,7 +235,7 @@ async def _named_plugin_action(request: Request, name: str, action: Callable[[st
@router.post("/api/dashboard/agent-plugins/update-all")
async def post_agent_plugins_update_all(request: Request):
"""Update every plugin with an update, unattended: a widened pin or a caution scan comes back
"""Update every plugin with an update, unattended: a widened pin or a git update's caution scan comes back
``needs_review`` (nothing changed) for the plugin's own Update action to walk."""
_require_token(request)
from hermes_cli.plugins_auto_update import update_all_unattended
+2 -1
View File
@@ -1893,7 +1893,8 @@ def _portable_server_names(plugin_dir: Path, key: str) -> set[str]:
def _plugins_update_all(rid, params):
"""Check every installed plugin and update each one ``hermes plugins update`` would move. Nothing is
accepted on the user's behalf: a widened catalog pin or a caution scan comes back ``needs_review``."""
accepted on the user's behalf: a widened catalog pin or a caution scan of a git update comes back
``needs_review``."""
_ensure_plugin_activation_listener()
return _ok(rid, _tools_mod("hermes_cli.plugins_auto_update").update_all_unattended())
+13 -7
View File
@@ -478,12 +478,15 @@ By default, plugins only update when you run `hermes plugins update`. Turning
on auto-update (`plugins.auto_apply: true`) lets the gateway's scheduled check
apply the same catalog re-pins and tracked Git pulls on its own. Both routes
use the update security scan, and an unattended update never accepts a widened
catalog pin, a caution scan verdict or new capabilities: those wait for you to
run the update yourself. Auto-update does not manage pinned, manual, drifted,
or pip-distribution rows. It runs on the gateway's housekeeping thread, so it
catalog pin, new capabilities or a caution scan verdict on a Git update: those
wait for you to run the update yourself. (A catalog re-pin accepts a caution
verdict, because a reviewer read that pin when it was admitted to the catalog.)
Auto-update does not manage pinned, manual, drifted, or pip-distribution rows,
nor a catalog plugin you installed with `--ref`: that stays on the commit you
chose until you run `hermes plugins update <name>`. It runs on the gateway's housekeeping thread, so it
never delays starting a session, and an offline check costs one warning.
Updated plugin code loads in new sessions; a running session keeps the code it
already imported. The setting is per profile.
Code the scheduled check updates loads in new sessions; a running session keeps
the code it already imported. The setting is per profile.
The same switch and an "Update all" action are on every surface:
@@ -495,8 +498,11 @@ The same switch and an "Update all" action are on every surface:
| Web dashboard | Plugins page, **Update all** | Plugins page, **Auto-update** switch |
`/plugins update`, Desktop and the dashboard update without asking per plugin,
so a plugin that needs a review (widened pin, caution scan) is listed as
"needs review" with nothing changed; update it by name to review it. Two
so a plugin that needs a review (widened pin, caution scan of a Git update) is
listed as "needs review" with nothing changed; update it by name to review it.
Each plugin they update is reloaded right away, as an update from the plugin's
own Update action is: its MCP servers and skills reach open chats now, its
Python tools load in new sessions. Two
updates of the same plugin never run at once: the second one is refused and
changes nothing.