ensure_import synced the extra into a new generation and then always
raised "restart Hermes to activate", so choosing a provider whose SDK is an
opt-in extra (anthropic, bedrock, ...) failed the first message in every
fresh install or worktree even though the install had succeeded.
adopt_selected() already knows when swapping a live process onto a new
generation is safe (it was on the generation selected before the sync, and
nothing it imported changed version). Call it after the sync; raise only
when adoption was refused, naming restart_needed()'s reason.
A process that could not adopt a newly published dependency generation keeps
importing the old one. restart_needed() names that case (and stays silent for
dev venvs, Nix and anything not booted from a PM generation, so no false
restart prompts); adopt_selected() lets callers move onto the selection before
loading new code. The test helpers publish real generations and make the test
process run from one.
(cherry picked from commit 978abe8ec4b852778b8c63bcefdf141db51bc703)
(cherry picked from commit 28be27334adc531db59bf37a02a64acaaf47a2ee)
After a plugin's runtime request is published, the process that asked should be
able to import the result without a restart, but only when nothing it already
loaded would change underneath it. Adopt when this process was on the
generation that was selected when the build started, and no loaded module's
file appears in the RECORD of a distribution whose version changed or
disappeared. Adopting swaps the site-packages entry, processes only the new
generation's new .pth files, rewrites PYTHONPATH/PATH for child processes, and
leases the new generation while keeping the old lease, since loaded packages
still resolve submodules from it.
(cherry picked from commit 478f945d9f36e94229f58b251642b7c9f3b9e781)
_get_anthropic_sdk() swallowed every ensure_import("anthropic") failure and
_require_sdk() then told the user to "Install it with: hermes pm install
--extra anthropic". PM often HAS installed it: sync_venv succeeds into a new
dependency environment that only activates at process boot, and
ensure_import raises "installed; restart Hermes". A lazy-install guard
("this process is not running from the install's dependency environment")
was flattened the same way. Users were told to install something that was
installed, or given a command that doesn't address the actual refusal.
Keep the import as the decider, but remember the InstallError and put its
text in the ImportError. bedrock_adapter._require_boto3 had the identical
shape; azure_identity_adapter already propagates str(exc) and is the model.
Under the updater's claim, sync whenever dependencies are not current
rather than only when nothing is committed. The tail's own children
and post-sync verification children are current and stay no-ops. A
stale generation still committed from the previous Python pin is the
same ABI trap as the pre-PM venv, and it now syncs too. If the sync
still leaves the tree out of date, raise instead of relaunching into
another sync.
prepare_launch returned early for any process running under the
updater's own claim, so it would not re-run the completion tail. That
also covered processes the updater spawns before PM commits a
generation (a restarted gateway), which then booted with no
environment: previously on the pre-PM venv, now refused.
Under the updater's claim with nothing committed, sync the dependency
generation (carrying the legacy venv's extras, as the first sync
always has), skip the tail since that belongs to the updater, and
relaunch on the store Python. The relaunched process sees the commit
and returns early as before, so the no-recursion guard still holds.
With nothing committed, activate_dependencies fell back to the in-tree
venv/.venv. After an update that venv was built for the old interpreter
(uv CPython 3.11) while the process ran PM's store Python 3.14, so every
compiled module in it was unloadable: the messaging gateway's Group Chat
worker died on `No module named 'pydantic_core._pydantic_core'` until PM
committed a generation ~40 minutes later and deleted the old venv.
committed_venv() returns the committed generation or a sealed payload's
environment, never the in-tree venv. Boot activation and child
activation environments use it. With nothing committed, a venv/Nix
interpreter keeps its own packages; PM's bare store Python refuses with
the repair remedy instead of running on inherited paths. selected_venv
keeps its contract because pre-PM updaters import it after the swap.
A hard-killed backend leaves its spawn-ledger record and its published
session token behind. Since the attach path started adopting published
tokens (f1247d2e01), such a record passed the token rung and then had its
refused port polled for the whole 45s readiness budget. The renderer's boot
timeout is the same 45s and starts first, so it gave up just before main
fell through to spawning, and every Retry/Repair killed the fresh backend
and produced another dead record: the app never started after an update.
A ledger record is written only after its backend binds, so a refused
connection on one means the process is gone. Readiness gains an opt-in
`alreadyBound` that fails at once on ECONNREFUSED; the attach probe and the
attached-backend liveness monitor set it. Remote/SSH readiness keeps
polling, since a tunnel still coming up refuses legitimately.
The extra removal left CI, the Docker image and the nix package still
requesting `hindsight`. Once the extra is gone, `--extra hindsight` and
extraDependencyGroups = [ "hindsight" ] ask for something that no longer
exists. Drop them the same way 73c598e319 originally did: remove it from
the CI extras lists, the Docker sealed-venv build and the nix default
groups, and point the nix examples/check at honcho. Also remove the
stray blank line left in the exclude-newer table.
Merge 27df3b8847 brought back the hermes-agent[hindsight] extra
(hindsight-client==0.6.1) that 73c598e319 removed. The catalog
Hindsight plugin now requires hindsight-client>=0.10.1,<1, so any
workspace containing it fails `uv lock`. Remove the extra, its
exclude-newer entry and its legacy-takeover mapping, and regenerate
uv.lock.
The workspace-member rename from the original PR is dropped here;
#122098 landed that half.
Salvaged from #122092.
4b7229d612 commits the default-brand icons, so build_source_web no
longer runs generate-icons.mjs. It updated test_source_build.py but
not test_web_ui_build.py, which uses the same source_products fixture.
The web build tests on main now expect a step that never runs.
Electron denies every target=_blank window, so a web link in a previewed
.md file looked clickable and did nothing; a #fragment link rode the
HashRouter and changed the app route instead of scrolling the note; a
relative link to a sibling note never opened it.
Links now take the same doors chat links take: web links render through
ExternalLink (in-app browser, Cmd/Ctrl for native), file links are wrapped
in the existing #preview/ hash before Streamdown's hardener sees them and
resolved against the note's directory by normalizeOrLocalPreviewTarget,
and headings get GitHub-style ids from a rehype plugin so a TOC click
scrolls within the pane. Slugs and lookups are NFC-normalized so a TOC
written on another editor still finds its heading.
Fixes#81055
Co-authored-by: xxxigm <tuancanhnguyen706@gmail.com>
Review suggestion from #122098: spell out on the condition line that
uv package mode produces real build metadata, so such members keep
their declared name.
record() looked up stamp['identity']['windowsExecutableName'], a key no
stamp writer emits, so every Windows record on a runner resolved no
executable and failed. The built package's Application/@Executable
names the exe; match it in the unpacked dir only, which also skips
before-pack's .bak rollback copy.
uv identifies a workspace member by its declared project name, so the
same plugin enabled in two profiles declares one name twice and the
dependency sync fails with 'Two workspace members are both named ...'.
Metadata-only members (no build backend) now carry the unique member
key in their name, exactly like manifest-only members already do. A
buildable member keeps the name it declares, since uv verifies it
against the package metadata its backend produces.
The 30s drain opened a throwaway gateway socket even when Bot Mode was off or the route had no outbox work. With the push door, only a route that signaled bot_relay.outbox.pending is drained. Older shells without that door still poll. The background-scope reset from #119836 is unchanged.
Importing a script from `node -e` leaves process.argv[1] undefined, and
pathToFileURL(undefined) throws on import. The bootstrap installer's
signing step imports batch-sign-binaries.mjs this way, and it crashed
when that pulled in sanitize-pe-signatures.mjs.
An authoritative still-pending clarify was answerable only after
getLatestSessionMessages returned. Publish the snapshot tool-call row
in the same needsInput view update, and keep provenance checks from
hiding that row.
The post-update relaunch refused a backend that had published a session token
and spawned another. Adopt the host rendezvous token when GET / withholds it.
The stale app.asar half is dropped: main now judges desktop freshness from the
compiler receipt written inside the packaged output (26c4e8b160), so a skipped
or failed rebuild no longer looks current.
A Telegram row that omits profile is owned by the backend that served
the messaging list. Keep a non-primary connection id, send a primary-pool
list through that profile door, and do not guess primary or ambient when
the list server was never recorded.
A dead or interrupted turn left the thinking spinner up, including after
a partial assistant payload. When that session stops producing events,
force-settle it and stamp the existing retryable error card instead of
leaving the spinner spinning.
Clicking a single-select choice leaves focus on the option button, and the
window keydown handler bailed for every button, so Enter never submitted the
staged answer. Exempt button[data-choice] and let activateActive handle Enter:
a staged single-select answer submits, a multi-select row toggles, and Continue
still confirms the set. Choice buttons do not call submitAnswer.
Fixes#92816
Co-authored-by: echohn <echohn@gmail.com>
The shared ['hermes-config-record'] slot survived a gateway switch, so a
settings save painted the previous machine's record and PUT it onto the
other config.yaml. Key that existing slot by $activeConnectionId.
Fixes#101640
Hiding inside the minimize handler wedges the Windows minimized flag, and
showInactive() restores a painted window that never takes input. Defer hide
until after that dispatch, skip a stale hide if the user already restored,
and activate with show()+focus() on Windows. Log main-process event-loop
stalls in desktop.log; renderer unresponsive handlers cannot see AppHangB1.
Reported in #119252.
Co-authored-by: finn763 <165816600+finn763@users.noreply.github.com>
Co-authored-by: KoNit-K <konit.block@protonmail.com>
Auto-speak keyed already-spoken on the assistant row id and ordinal.
Hydration rewrites the live id and tool-row folds move that ordinal, so
the same turn looked unspoken and was played again. A second
playSpeechText stopped the first clip. markSpoken also ran before a play
that never started, so the turn stayed silent.
Key the anchor on the user turn, which survives both. A later turn that
says the same thing is still spoken. Release the anchor when playback
never starts, and do not let a second start of the same turn stop the first.
An empty connection id is no longer rewritten to registry.primary, which dialed
another SSH host. A concrete remote-only profile, including default, is refused
on the forced-local branch instead of spawned. A profile open without an owner
route no longer stamps mode local when the live connection is remote.
A second Desktop window on the same stored session never saw the first
window's completed turn, and submitting from that stale transcript could
fork the session. Notify other windows to re-pull on turn completion, and
refuse composer and session-tile submit when the local transcript is behind
the authoritative latest page.
Co-authored-by: stantheman0128 <stanshih888@gmail.com>
Desktop speak-stream sent type=fallback whenever the provider had no
chunked PCM API. Edge is that case, so the client waited for the full
reply and POSTed it. Cut sentences with the existing sync TTS tool and
stream that PCM. Fallback stays the last resort when synthesis produces
no audio.
Refs #91997
A profile that finished work (or blocked on input, or is still running)
while another profile was selected showed no indicator on its rail square
or dropdown row — the session-level unread/attention layers
($sessionDotStateById, the persisted per-profile unread markers, the
backend row.unread watermark) all existed, but the profile rail
subscribed to none of them.
Add $profileDotStateByScope: a per-(connection, profile) rollup of the
shared session dot state, derived in three passes:
1. loaded chat/messaging rows claim their row-tagged scope
(stalled/background fold into working, like the sidebar's buckets;
cron rows stay excluded so a profile square is not a cron counter),
2. unlisted live runtimes claim the scope their socket proved
(runtimeSessionOwner; no proven owner claims nothing),
3. persisted unread markers with no loaded row claim their profile's
scope only when the owning gateway is unambiguous — two gateways
sharing a profile name leave the bucket unpainted rather than
guessing.
Priority: needs-input > working > unread, matching the session rank.
The busy->idle finish edge now passes the socket-proven owner profile
into markSessionUnreadFinished, so a background profile's finish can no
longer land in the ACTIVE profile's marker bucket (which would light
the wrong square). The reconnect parked-set becomes a Map so a later
confirm keeps the runtime id for that lookup.
The rail paints the rollup as a small dot on ProfileSquare, RestSquare,
ProfilePill and both dropdown rows, with every non-zero count in the
accessible name and tooltip ("writer, 1 unread session"). The active
profile's square stays clean — the workspace is homed there and its
rows are on screen in the sidebar below.
Attached images rendered at a 512px thumbnail in the live sent bubble and
click-to-zoom lightbox, only becoming sharp after a session reload rehydrated
the turn from disk. Route the in-flight bubble through the same DirectiveImage
path as a reloaded turn: a bounded thumbnail is painted inline (preserving the
deliberate anti-freeze cap) while the full-resolution file is handed to the
on-demand lightbox and download.
Fixes#93204
Review points:
1. Noisy shell stdout (curl -v, build logs) is kept from flooding the panel:
every prose candidate passes through looksLikeArtifact, which requires a
file/image extension or http(s)/data: scheme, and local file existence is
resolved via the media ladder (artifactImageSrc -> resolveMediaDisplaySrc
-> readFileDataUrl). Documented at the shell-output scan site.
2. Bare numeric array indices are dropped from the key path intentionally so
array-of-results payloads (e.g. outputs.0.output) match via their real
segments; noted that switching to exact-key matching would silently break
those shapes.
Terminal results were never scanned for artifact references: 'terminal'
is not matched by ARTIFACT_PRODUCER_TOOL_RE, and its stdout lives under
the bare 'output' key which STRONG_TOOL_ARTIFACT_KEY_RE does not list.
Script-generated figures (matplotlib, ffmpeg, pandoc, ...) never showed
up in the Artifacts panel unless the assistant remembered to re-emit a
MEDIA: tag in prose.
Treat terminal as an artifact producer: scan its free-text result
(MEDIA tags, markdown refs, URLs, absolute paths) and accept
'output'/'stdout' as scannable keys for that tool only. Non-terminal
tools keep their existing strict key gate.
Fixes#92220