asRpcError now always wraps a non-string name in a fresh Error. In-place
assignment was a silent no-op on sealed objects in sloppy mode. Catch
only host.request / requestProfile so routing TypeErrors keep their stack.
JSON-RPC/IPC can reject with a plain object whose name is a number.
React 19 then crashes on (error.name || '').trim, which takes down the
Routines pane instead of showing the cron.manage failure. requestForBot
now wraps those values in an Error with a string name, including
cross-realm Error-like objects from the plugin test vm.
The SDK's focusedSessionOwner store fails closed to null whenever the
focused session has no unique bot owner (a normal chat, ambiguous owner
hints) - the common case while the user browses the Bots pane.
resolveRoutineOwner treated that null as an error and returned null
before consulting the roster selection, so the Routines pane pinned
every agent on 'Cronjobs are unavailable until this agent appears in
the roster.'
Drop the fail-closed null gate and fall through to the existing
selection ladder (focusedBot || selectedBot || ...). An authoritative
focused owner still wins through its exact roster row and still fails
closed when that row is absent; a null owner with no matching selection
also still fails closed. Regression tests prove red pre-fix, green
post-fix.
Electron safeStorage parks a per-app key ('Hermes Key') in the macOS login
keychain; on machines with a locked/missing/corrupted default keychain that
turned every Hermes Desktop launch into a blocking 'Keychain Not Found' /
password dialog. Keychain-backed encryption is now an explicit opt-in:
- electron/secret-storage-policy.ts: standalone policy seam (default OFF,
strict === true coercion, one-shot migration flag) + unit tests
- default path never calls any safeStorage API (including
isEncryptionAvailable, which itself touches the keychain)
- one-shot legacy migration decrypts existing safeStorage blobs to plain
0600 files at first launch; undecryptable blobs are kept but read as
absent afterward (classify 'drop') so a dead keychain prompts at most once
- Settings -> Gateway toggle (all 5 locales) re-encodes every stored secret
store in place when flipped (v1 connection.json, v2 connections.json,
native-oauth-tokens.json)
- e2e: at-rest spec now covers both postures (opted-in unchanged contract,
default saves without secure storage, owner-only bits, restart round-trip)
- docs: multi-connection-desktop + desktop-native-signin updated
Sidebar and legacy session-list helpers tagged the registry connection but
not the active profile, so Electron routed those reads to the wrong backend
after a profile or remote switch.
Keep hermesApi connection-only: stamp profileScoped on the list helpers
instead of every REST call.
Co-authored-by: noah <loahnisk@gmail.com>
Managed SSH maps a Desktop profile label onto a different remote name.
The sidebar filter lives in recents_profile, so rewriting only ?profile=
left those reads on the remote default and the Sessions list came back empty.
Co-authored-by: noah <loahnisk@gmail.com>
The diff-lines error-boundary test mocked './syntax-diff' with a factory
that THREW. vitest hoists the factory and registers its module promise in
the mocker registry; a throwing factory leaves rejected promises there,
and under CI load one escapes as "Vitest caught 1 unhandled error during
the test run" attributed to whichever sibling file the worker is running
(user-message-edit.test.tsx in run 32803716726) — an intermittent js-tests
red on green code.
Rework: the factory now resolves to a component that throws the fetch
error during render — the same way React surfaces a rejected lazy payload
— so no rejected promise ever sits in the registry.
Guard proof (sabotage A/B): with the local syntax-diff ErrorBoundary
removed from diff-lines.tsx, the reworked test still fails (workspace
fallback renders), so the #93479 regression pin is intact. Full ui
project: 596 files / 5729 tests green, no unhandled errors in 3 full-run
repetitions.
The #94219 replay was a production no-op: the server returned full
JSON-RPC envelopes from session.events.since while the client's replay
loop dispatches only elements with a top-level 'type' — every replayed
event was silently skipped. Each side's tests validated its own
assumption, so both suites stayed green.
- server: events_since() now returns bare event objects (the frame's
params), the exact shape the live dispatch path consumes; ring stores
params directly; cross-language contract test added on both sides.
- client: live frames racing an in-flight replay are parked and flushed
seq-gated afterward — no double dispatch of deltas, no gap-skip from
a watermark advanced past the replay window.
- restart poisoning: seq counters are in-process, so a backend restart
reset them while clients kept high watermarks (replay forever empty,
truncated=false). New replay_epoch advertised in gateway.ready and
echoed by session.events.since; the client clears watermarks on epoch
change.
- methods_session no longer reaches into event_replay privates
(is_truncated() accessor).
Live repro: pre-fix, 3 stamped frames -> 0 dispatchable by the client
gate; post-fix 3/3. Tests: 16 py (replay+ws), 8 vitest, tsc clean, ruff
clean.
web_extract stopped using an auxiliary LLM long ago (deterministic
truncate-and-store), but browser snapshots still routed oversized
accessibility trees through the auxiliary web_extract model, keeping a
dead-looking aux slot alive across every config/picker surface.
- tools/browser_tool.py: remove _extract_relevant_content and
_get_extraction_model; oversized snapshots always truncate at line
boundaries, store the full tree to cache/web, and append a read_file
pointer (element refs beyond the cut live in the file)
- tools/browser_camofox.py: same — no LLM path
- Remove auxiliary.web_extract slot: config_defaults (removal note, same
pattern as session_search/PR #27590), cli.py defaults + env bridge,
gateway/run.py bridged keys, hermes config display, hermes model picker,
dashboard REST slots, desktop + web AUX_TASKS, i18n labels (en/zh/
zh-hant/ja/ar)
- Docs: env-vars, configuration, fallback-providers, browser + zh-Hans
mirrors (web-search zh-Hans was stale on the old LLM pipeline — synced
to truncate-and-store truth)
- Tests updated: aux bridge uses approval slot, browser tests assert the
LLM path is gone and stored files are secret-redacted
Drives the reported path rather than the helper: set a non-default
scale, then navigate to routes Chromium holds no zoom record for, which
is what opening a new session looks like to the per-URL store. Keeps the
Cmd/Ctrl+N case alongside it.
Co-authored-by: Clark Vines <38430798+clarkvines@users.noreply.github.com>
Desktop is a HashRouter over one file:// document, so every route is a
distinct URL to Chromium's per-URL zoom store. A route the user never
zoomed on has no record at all and resolves to the host default (100%) —
that is every fresh session and every never-visited settings tab.
In-page navigation fires neither did-finish-load nor any window event,
so nothing re-asserted the persisted level. The window dropped to 100%
while the Appearance control kept reading the chosen scale, because the
renderer only learns of zoom changes through 'hermes:zoom:changed',
which never fired. Touching the setting sent a fresh apply, which is
why it appeared to fix itself.
Re-assert the persisted level on main-frame did-navigate-in-page.
Verified on real Electron 40.10.2 / Chromium 144 (win32): a recordless
hash route reports 100% at the event, so the existing drift-guard sees
the drop and re-applies, and still no-ops when the route's record
already matches.
Fixes#48658Fixes#38854Fixes#79863
Co-authored-by: Brooklyn Nicholson <brooklyn.bb.nicholson@gmail.com>
The dom context menu disabled Paste unless a renderer-side
readClipboard() probe reported text when the menu opened. The items
action never consumes that probe: editableCommand("paste") dispatches
webContents.paste() in main - the same Chromium path Ctrl+V takes, which
resolves the system clipboard itself. On Windows the Win32
clipboard.readText() bridge can return empty while that path succeeds,
so Paste stayed grayed out even though pasting would have worked; probe
errors were swallowed the same way (.catch(() => undefined)).
Fail open instead: drop the gate and the now-unused clipboardHasText
fact from the dom menu shape, so opening an editable menu no longer
makes the IPC round-trip at all. Pasting with an empty clipboard is a
harmless no-op, matching Chromiums own menu, which keeps Paste enabled
for editables. The terminal paste item keeps its gate - its action
inserts the readClipboard() text into the PTY directly, so there the
probe and the action share one mechanism and the gate stays honest.
Fixes#91553
The overlay window's playVibeHearts() only fires on a reaction forwarded by
burstVibeHearts, so the single gate covers it — these tests pin that so a
future direct caller shows up as a red test.
Floating affection hearts were always on with no off switch. Message
Reactions in Appearance looks related but only gates message-row
tapbacks. Add a separate Vibe Hearts preference (default on) next to it.
`read_window_below` answers "could not enumerate windows on this system" on
macOS and Windows whatever went wrong, and the three failure paths behind it
discarded their errors — so a report where the HUD could see nothing had no
way to distinguish the module failing to load, the helper failing to spawn,
and the OS answering with nothing. Three different fixes, one sentence.
Enumeration now returns the reason, the tool's error carries it, and the HUD's
game-overlay watch logs it once before it gives up (it retries twice and then
goes quiet forever, which is the other half of why the log said nothing).
Linux keeps its environment-derived advice, which is more actionable than the
raw exception.
The frost is the whole window rectangle and the `[data-hud-glass]` scrim is
what makes it readable, but the two ran on different gates: the scrim is
focus-only, while the caller widened the frost to "recent or held" — i.e. for
the whole of a turn. Thinking with the composer unfocused therefore raised a
bare native material with no scrim over it, which on a light theme is a white
slab under the band's unconditionally white ink.
Put the frost back on the scrim's gate, and re-run it on the window's own
focus changes: clicking away to another app fires no focusout, so the scrim
would go while the frost stayed behind.
Server: per-session monotonic seq on every routed event frame, bounded
512-frame replay ring (64 sessions, FIFO eviction), plus two new RPCs —
session.events.since (replay newer-than-watermark, reports latest_seq +
truncated so clients detect gaps) and session.events.stats (telemetry).
Client: per-session seq watermarks recorded from live frames; after any
successful reconnect a fire-and-forget fetchReplay() drains missed events
through the normal dispatch path (recordSeq ignores non-increasing seqs,
so stale replay can never regress a watermark); focus-triggered reconnect
nudge in use-gateway-boot for the Electron unfocused case where macOS wake
skips visibilitychange.
Replay failures are swallowed by design: lossless resume is an upgrade
over the previous lossy reconnect, never a new failure mode.
Replay single-question and batch snapshots from session.activate/resume,
including locked answers, and extract the helper so the session-actions
god-file is not the only owner of that wire shape.
Co-authored-by: ClintonEmok <54935030+ClintonEmok@users.noreply.github.com>
Co-authored-by: frendo <frendo.wu@gmail.com>
A hydrated Ask/clarify row stays complete after session or bot switch, so
the live card never mounts. Re-arm the existing transcript row and keep
the provider tool id instead of appending a duplicate at the tail.
Co-authored-by: frendo <frendo.wu@gmail.com>
Clarify remounted as a tool row once session.info flipped running=false, thinking previews collapsed their body, and the duration line grew the footer.
Cover the null-path draft path, Home-scope createBackendSessionForSend,
and openNewSessionTile({ cwd: null }) so the last project folder cannot
leak back into a Home chat.
Home's "+" passes path/cwd null on purpose, but null was falsy and fell
through into resolveNewSessionCwd(), so "New session in Home" (especially
the openTab path while main chat is occupied) still created under the
previous project folder and showed its branch.
Browser tabs took the lowest free slot, so an id was reused once its tab
closed. That is only safe while every store keyed by the id is wiped on
close — true today, but a discipline rather than a guarantee, and stale
state would resurface under an unrelated tab the day it lapses.
Mint like a terminal does instead: no id is ever handed out twice.
Committing an address dropped the field back to the url of the page you
were leaving, so typing baby.com over google.com flashed google.com back
before baby.com arrived — and nothing said a load was underway.
The address you asked for now stays in the field until the page actually
lands somewhere (a redirect supersedes it, as it should), and progress
spins inside the field beside it. The pane owns that loading state from
the moment it accepts the address, because the reach probe it runs first
delays did-start-loading.
A URL tab used to be a singleton — every link navigated the one Browser,
so there was no way to keep a page open beside another and the strip's
"+" never appeared next to it.
A Browser tab is now a vessel with its own id: links still land in the
browser you are looking at (an agent opening five pages must not leave
five tabs behind), while the "+" mints another one on request. Tabs name
themselves after the page they are showing, since three tabs reading
"Browser" name nothing.
The "+" itself is now a pane capability rather than a session-only
button, so any pane kind that can make more of itself contributes one.
Move, ignore-mouse, placement, resize edges, snap, cursor feed, and
overlay promote all read the same Ozone-normalized capabilities instead
of re-deriving linux/Wayland/X11 at each call site.
The chord previously only acted when a terminal or preview selection
existed. A new bubble-phase window fallback now moves focus to the
composer on an unclaimed press, like the address-bar chord in a browser.
Existing owners keep priority: selection handlers claim the press on the
capture phase, a user-rebound action marks the event handled, and a
focused terminal with no selection keeps Ctrl+L as clear-screen via
composerFocusBlockedBySurface().
The chord matcher moves from the terminal feature to
src/lib/keybinds/chords.ts as isComposerChord: it now has three
consumers and the old name (isAddSelectionShortcut) was wrong at the
composer call site. The fixed panel row view.terminalSelection becomes
view.selectionToComposer because it covers preview selections too.
Omarchy tiles the HUD like any other toplevel, so always-on-top and
xdg_toplevel.move never apply. Ask Hyprland to float+pin after map,
trying classic dispatch then Lua for 0.55+ configs.
A start-marker probe failure (Get-Process timing out on a PowerShell 5.1
cold start, #87169) in claimBackendChild used to stop the freshly spawned
backend and rethrow — killing a healthy backend, triggering the renderer's
repair respawn, and looping. And because stderr piping only attached after
the claim, every before-ready failure surfaced as a bare exit code.
- extract probe + claim policy into electron/backend-claim.ts:
processStartMarker/execText (moved verbatim from main.ts), probeStartMarker,
and a pure claimDecision(childAlive, probe) a Windows CI lane can drive
with real PowerShell
- probe failure + LIVE child now degrades to PID-only identity
(pid-only:<pid> marker, WARNING logged), matching the existing
createParentStartMarkerResolver degrade pattern; processIdentityMatches
verifies degraded identities by PID liveness (command check still layers
on top in backendIdentityMatches)
- probe failure + DEAD child keeps the fail-closed throw, now carrying the
child's buffered stderr/stdout tail
- ring-buffered ~8KB output tail attached at spawn time in BOTH spawn paths
(pool + primary); tail appended to claim errors, before-ready exit
messages, and backend-ready's exited-before-port-announcement errors so
the real exit reason reaches desktop.log and the boot UI
- tests: claimDecision matrix (degrade test fails against the old
stop+throw behavior), real processStartMarker probe, ring-buffer caps,
and output-tail suffixes on backend-ready exit errors
A persisted tall/narrow size has no way out on Linux. Put a reset next
to Exit HUD so the default size (and position, where the compositor
allows it) is one click away.
Co-authored-by: Shawn Wang <32839114+enwaiax@users.noreply.github.com>