`mermaid.render()` returns SVG aimed at inline HTML, and its sanitizer
gives back a `<br/>` written in a label as an open `<br>`. That string is
not well-formed XML, so the strict `image/svg+xml` parse in
`normalizeSvgSize` yields the parsererror document instead of an `<svg>`,
takes the `el.tagName !== 'svg'` early return, and hands the *unnormalized*
string back. The diagram `<img>` then gets it as a `data:` URI, which Blink
decodes as strict XML and rejects for the same reason — so the message shows
a broken image whose alt text reads "Mermaid diagram", with nothing thrown
and nothing written to any log.
Close HTML void tags before the markup reaches either XML parse. The pass is
generic rather than a `<br>` special case, because the same class of input
reaches `<hr>`, `<img>`, `<input>`, …, and it is idempotent: already-closed
tags and non-void tags (including names that merely start with a void tag,
such as `<break>`) pass through untouched.
Fixes#133089
Verified by replaying 32 real ```mermaid fences from a session database
through the app's own renderer chunks in Electron 40.10.2 / Chromium 144:
24/32 fail before this change — every failure has `<br/>` in a label, with a
perfect 1:1 correlation — and 31/31 renderable diagrams succeed after it. The
32nd is an invalid Mermaid source that correctly keeps falling back to its
source block.
An interrupted detached turn sets the client-gone and turn-cancel latches. If
the active /loop or /heartbeat then re-armed the orphan timer after the turn
settled, neither latch ever cleared: no tick could claim the turn and every
reattach was refused with 4009, so the loop never reached its budget and the
session stayed pinned. Collect the settled session as before instead.
Also: TTL eviction checks the cheap clock before the store-reading exemptions,
and the heartbeat branch gets its own reaper tests (active local pins; paused,
cleared or gateway-owned releases).
The per-session notification poller is the only driver of a Desktop/TUI
(route-less) /loop or /heartbeat. When the client disconnects, the WS-orphan
reaper collects the idle session ~20s later and the TTL/LRU reapers can
collect it after that; either stops the poller, so the schedule freezes
(status still 'active') until a client reattaches and the overdue tick fires.
Treat an active, non-gateway-routed loop or heartbeat like live delegated
work: the orphan reap re-arms instead of popping the session, and the TTL and
session-cap reapers skip it. Gateway-routed schedules are fired by the
gateway's own scanner and do not pin the session. The exemption is bounded by
the loop's tick budget and ends on stop/pause. Memory-trim quiescence is
unchanged: an idle looping session does not block a trim.
Every source update installs Node dependencies as one npm ci over ui-tui,
web and, when a built desktop app is present, apps/desktop. Only
apps/desktop brings native addons (node-pty, electron), so a host whose
compiler cannot build them (Ubuntu 20.04's g++ 9 rejects -std=gnu++20)
failed the whole install and lost the TUI and web UI builds as well, with
"a source update could not be finished automatically" on every launch.
When the union fails and the desktop app is part of it, retry once without
apps/desktop: the TUI and web UI build, the desktop build is reported owed
("desktop dependencies failed", the line the hand-off scripts match), and
the update names `hermes uninstall --gui` for hosts that never run the
app. The same hint prints whenever a headless Linux update builds Desktop,
since such a host keeps rebuilding an app nobody launches.
Without the opt-in, external recall is kept whole up to
max(10 x hooks.output_spill.max_chars, 100k chars) and still spills above
that, so one runaway provider can't overflow the context window and wedge
the session. Normal provider-budgeted recall (~16 KB for Honcho) is
unaffected.
hermes gateway restart --all (and status, cron's gateway check, the update
fleet sweep) matched gateway argv host-wide with no owner check, so a gateway
run by a different UID became a SIGTERM target ('Permission denied to kill
PID') and was reported as an unfolded per-profile gateway with
'hermes gateway migrate --multiplex' advice. Non-root callers now skip
processes another uid owns; root keeps the host-wide scan (sudo over a
User= system unit), and an unknown owner keeps the argv verdict.
A memory provider's own installer (mnemosyne-hermes install, its README's
manual `ln -s`) leaves ~/.hermes/plugins/<name> as a symlink into a venv. The
catalog install of the same name then failed in _sanitize_plugin_name with
"Invalid plugin name 'mnemosyne': resolves outside the plugins directory" and
was recorded as failure_class=manifest_invalid, though the catalog manifest is
valid (hermes plugins validate passes it). Name the link and its target, say how
to free the slot, and record already_installed.
recover_at_startup tried the configured provider's catalog install once per
PROCESS, so a home whose install fails (PM resolve/clone error, offline) retried
and failed again on every `hermes chat -q`, cron run, gateway agent and Desktop
restart. Each attempt is a recorded hermes.extension.install.count failure:
hindsight logged 1048 failures over 204 failing install-days (~5 per day per
install, the retry-loop shape) after it left core.
The left-core migration already had the fix: a failure stamp under
<home>/cache gates startup attempts for an hour while `hermes update` always
retries. Move that stamp into memory_provider_migration and use it in
recover_at_startup too; the backed-off start still tells the user memory is
off and names the install command.
text_to_speech swept every path it synthesized into on failure, including
an output_path that already held a user's file. When the target already
exists, synthesize into a hidden sibling instead; the delivery step still
os.replace()s it over the target on success (overwrite contract kept),
and a failure sweeps only the sibling.
The discovery heuristic only searched the first 8 KB of a plugin's
__init__.py for the provider contract, so a valid provider with a long
module docstring was invisible. Fall back to _defines_memory_provider
(AST parse, no import) when the cheap text search misses.
Plugins can now put a short plain-text line in the core pet's speech bubble
instead of locating the pet canvas in the app DOM and floating their own
overlay over it (pet-wallet, #135178).
- ctx.pet.say(text, { id?, tone?, ttlMs? }) -> disposer; ctx.pet.clear(id?);
ctx.pet.visible (ReadableAtom<boolean>)
- host-owned rendering in PetBubble, in-window (plugin lines only) and in the
pop-out overlay (lines ride the existing pet-state IPC push)
- plain text, 120-char cap, control/bidi chars stripped, plugin-name label,
TTL 6 s default (1-30 s), 3 live lines and 10 says/10 s per plugin
- core error / waiting states keep the bubble; nothing shows without a pet
- every line a plugin has up is cleared on unload/disable/reload
- docs: API section, migration-table row for pet-wallet, SDK exports
A stop racing CapturedCDP.call() can close the supervisor loop before the
queued _send runs; run_coroutine_threadsafe's future then never resolves and
fut.result(timeout=None) hangs the consumer thread forever. Poll the unbounded
wait and raise CapturedCDPInvalid once the loop is closed. Fold the salvaged
validation tests into the dispatch file (one fixture, no F811), and tighten
them: the deadline test stalls inside the result grace, socket close asserts
CapturedCDPInvalid, the async validator asserts the refusal message.
The connection-card / manage_connections install of an OAuth catalog entry runs the probe on a
worker thread; `mcp_oauth.start` then rebuilt the worker's error as a bare RuntimeError from its
text, dropping the `failure_class` `_probe_single_server` had tagged. Every pre-authorization
failure (empty or schemeless URL, unreachable host, 404 endpoint, refused client registration)
was recorded as `exception:RuntimeError` -> `exception`.
- run_worker classifies the original exception with probe_failure_class and stores the class on
the flow; start() re-tags the RuntimeError it raises with it.
- probe_failure_class reads InvalidMcpUrlError (not http(s)://host) as config_invalid.
- _require_rendered_remote tags an unset ${VAR} in url/headers as missing_credentials.
- the "server responded but no token" refusal is auth_required.
- a connect_failed message that does not name the server gets a one-line next step
("Could not connect to 'n8n-official' (Not Found). Check its URL ...").
n8n-official is the only catalog entry whose URL is a user-supplied `${VAR}`, so it is the only
one where these user-side errors happen before authorization.
Follows the review on the plugin at 70fc4df6: requires_env trimmed to GITHUB_TOKEN
(the other four are optional config, and AGENT_BATCH_STATE is a directory override,
not a secret), the two agent tools the plugin now registers are declared, the
description states what actually phases the list, and the free-runner claim corrected
to the runners' real auth paths. Re-pinned to 064afc5d.
Pins the qdrant memory provider at f65e62d (0.1.8), which adds display.level
(off | summary | verbose) for the recall indicator. The description's
previously-unqualified 'progress display (off/minimal/verbose)' sentence is
replaced: it named the older top-level progress knob, which is now easily
confused with the new display.level ladder. Both are stated with their own
value sets and what each gates.