Commit Graph

33591 Commits

Author SHA1 Message Date
Teknium
52fb4e0877 fix(desktop): read Bot Mode avatars over the active socket, not one dial per bot
useRoster repaints every 5s and hands pullServerAvatars the active-source
rows. Since the multi-source merge (ed20a6f01a) every such row is
sourceScoped, so the avatar sync branch chose requestForBot and dialed each
bot's OWN backend to read a profile-directory PNG: a fresh WebSocket with
one JSON-RPC message, torn down at refcount 0, per bot per tick (#99336's
"ws accepted / ws closed messages=1" every ~5.2s on background profiles),
and for every bot with no running backend a pool spawn that waits out
POOL_SLOT_WAIT_MS (30s) and is re-queued by the next paint, forever, once
the pool is full (#102913's per-bot "waiting for a free local slot ...
timed out" cadence). The loop was self-sustaining because the plugin's own
160px face raster is deliberately not parked in $botMeta, so the empty
image slot re-fetched it on every tick.

Assets are files under the profile directory; the gateway that just
answered profiles.list reads them for any of its profiles. Route the three
avatar RPCs through host.request like the roster query itself, and remember
face-only answers so a row is fetched once, not once per tick.

Not changed: relay.ts (its loops dedupe to one route per registered
connection and return early below two connections, so a single-connection
desktop never issues a relay RPC), and useRoster's own profiles.list, which
already rides the active socket via requestForBot({name}).
2026-09-11 06:21:24 -07:00
Teknium
e4080b381b fix(desktop): roster avatar sync no longer dials a backend per registered profile at launch
The first Bot Mode roster paint after launch ran pullServerAvatars over every
row, and for a source-scoped row (every row on a local-primary desktop once
host.agents annotates the roster) each profiles.get_asset / set_asset went
through requestForBot -> host.requestProfile -> requestGatewayForAgent, i.e. a
(connectionId, profile) secondary that spawns that profile's pooled backend.
With ~60 registered profiles and 3 warm slots this queued 56 background spawns
at boot; each queued dial then rode reconnectSecondary's backoff until the
stall budget parked it (#107969), so the pool never drained and desktop.log
filled with "waiting for a free local slot" (#102978).

The active gateway's own profiles.list already produced these rows by reading
every local profile directory; get_asset/set_asset are the same directory
reads addressed by name. Route both through host.request on the active socket
with the row's backend profile name (route.targetProfile, so managed aliases
still resolve). No secondary socket, no pool slot, no spawn.

Cross-connection (remoteSource) rows never reached this path: pullServerAvatars
is fed activeSourceRoster, which filters them out.
2026-09-11 06:21:24 -07:00
kshitijk4poor
ff59fcd710 fix(state): skip the display-trigger drop+recreate when the triggers already match
DEFERRED_INDEX_SQL unconditionally DROPs and re-CREATEs the four display
triggers on every open so a changed trigger body rolls out; DROP TRIGGER IF
EXISTS on an existing trigger takes the write lock, so a settled database
still blocked behind a sibling's transaction after the three statement
gates. Compare each trigger's stored sql against the desired CREATE text and
run the pair only when they differ. CREATE ... IF NOT EXISTS forms are
lock-free on an existing object and run as written. The no-writes test now
also traces DROP TRIGGER / ALTER.
2026-09-11 06:21:00 -07:00
kshitijk4poor
c4c7e102aa refactor(state): tighten generation-stamp comment to the why 2026-09-11 06:21:00 -07:00
kshitijk4poor
b8d7977f0d test(state): trim #101881 tests to two invariants (zero writes on settled open; open not blocked by sibling write lock) 2026-09-11 06:21:00 -07:00
John Paul Soliva
ef8d682200 perf(state): stop taking the state.db write lock to open a database that needs no writes
Every read-write SessionDB open issued three writes that usually change
nothing, and a write statement takes the database write lock even when it
matches no rows:

- _ensure_db_file_generation's INSERT OR IGNORE into state_meta. The stamp
  is minted once per FILE, so every open after the first inserted nothing.
- the NULL-`active` heal, UPDATE messages SET active = 1 WHERE active IS
  NULL, which matches nothing on a healthy database.
- the fts_storage_version stamp, which re-wrote the same value on every
  open of an already-optimized database.

The connection is opened with timeout=1.0, so each blocked write costs a
full busy timeout while a sibling process holds the write lock, and the
open path's patience loop can ultimately give up and raise.

Gate all three on a read. Measured on a real 99 MB state.db (239
sessions, 6470 messages) with a sibling holding the write lock: 2117-2136
ms -> 3.6-7.1 ms. On an already-optimized database the unpatched open does
not merely stall, it raises `database is locked`; patched it completes in
3.7-6.8 ms. With a sibling running 200 ms write transactions in a loop
(n=20 opens): p50 894.6 -> 9.6 ms, p90 1094.7 -> 13.9 ms. A settled
database now issues zero main-database write statements to open.

The reads cost nothing measurable: the state_meta probe is a primary-key
seek (2.0 us), the messages probe is 1.7 us on the modern NOT NULL column
(unsatisfiable constraint, short-circuited) and 0.4-0.6 us at 300k rows on
a legacy default-less column via the partial index that already exists for
exactly this predicate. An uncontended open is unchanged.

Semantics are preserved. INSERT OR IGNORE still resolves the first-opener
race inside SQLite and racers still converge on the winner's token via the
re-read; the application_id gate and the PASSIVE-only checkpoint are
untouched; the heal is still considered on every startup, as #60108
deliberately made it, with only the write now conditional on a read
proving there is something to repair.

Read-first also fixes a correctness bug. Under contention the generation
block was abandoned by its `except sqlite3.Error` handler, so a process
ended up with no generation token at all even though the value was already
on disk and a plain read would have returned it -- and that token feeds
the deleted-WAL and replaced-file guards added by #101221. The heal's
`except OperationalError: pass` likewise skipped the repair silently, so
the unconditional form did not even deliver the unconditional repair it
advertised whenever it mattered most.

The probe deliberately does not use INDEXED BY: that hint raises
OperationalError("no query solution") against the modern NOT NULL column,
and the existing handler would swallow it, disabling the repair forever.
2026-09-11 06:21:00 -07:00
Teknium
dbc5d7c60b fix(desktop): warmAgent goes through the guarded prewarm resolver too
host.warmAgent — the (connection, profile) sibling of warmProfile that
bot-row.tsx fires on pointerEnter for multi-source roster rows — still
dialed openGatewayForAgent directly, so a pointer sweep across a mixed
roster kept spawning at pointer speed past maxBackends on the registry
path even after warmProfile was guarded. Same bug class as #103631,
different door.

prewarmProfileBackend now takes an optional connectionId: the
active-profile no-op, the 60s throttle (keyed by the pool scope key) and
the pool-saturation skip apply unchanged, and the dial picks
openGatewayForAgent for a scoped source. One resolver owns every
speculative warm in the app; the real click still spawns on demand.
2026-09-11 06:20:30 -07:00
Abdulrahman Jahfali
b23559877a fix(desktop): route host.warmProfile through the guarded prewarm resolver
Plugin rosters warm profile backends on pointerEnter with no dwell of
their own. warmProfile dialed openGatewayForProfile directly, bypassing
the pool-saturation guard, hover dwell, and per-profile throttle that
prewarmProfileBackend enforces for the built-in rail — so a pointer
sweep across a roster could spawn past maxBackends and leave the next
profile's real spawn queued until the 30s slot timeout, surfacing as a
profile surface that hangs forever while every other profile renders.

Delegate to prewarmProfileBackend so every speculative warm shares one
resolver and one policy, as the design guide requires. The real click
still spawns on demand; only the speculative head start is gated.
2026-09-11 06:20:30 -07:00
brooklyn!
8706517544 style(desktop): lint and format the salvaged titlebar files 2026-09-11 08:00:29 -05:00
abundantbeing
837e4b0942 feat(desktop): let Appearance choose left or right for titlebar app actions
Settings, Layout, and HUD default to the right so tabs keep the left
titlebar. Appearance has a Left/Right control for people who want the
previous left cluster.

(cherry picked from commit 7fe3175e475eb0ea81198bb69a0250662f940b17)
2026-09-11 08:00:29 -05:00
abundantbeing
a09368fcd6 fix(desktop): pin settings layout and HUD back to the right titlebar
Keep panel tabs in the titlebar moved those app actions next to the
sidebar toggle, which ate the tab strip. Put them back on the right
edge. Sidebar toggle stays left. Fixes #107351.

(cherry picked from commit 7f4460a7f6028cf384506733a5bfa52273792d64)
2026-09-11 08:00:29 -05:00
Teknium
75a6fac052 fix(desktop): focus_pane un-minimizes the tree zone for every revealer
`revealDesktopPane` drove files/review/sessions/terminal through their
store setters only. Those are same-value no-ops when the pane's `$open`
already reads true while the user minimized its zone from the header
chevron, so the `focus_pane` tool reported success over an invisible
pane (#106009; class noted by @worryfreeaa). Route every tree-backed
revealer through `revealTreePane` after its own setter, which clears
`minimized` and fronts the pane.

(cherry picked from commit 690a1a75108ec63ce5cb1a638543969b0877dbaa)
2026-09-11 08:00:29 -05:00
hermes-seaeye[bot]
aabad7b042 fmt(js): npm run fix on merge (#108217)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-09-11 12:59:42 +00:00
hermes-seaeye[bot]
efca39279a fmt(js): npm run fix on merge (#108214)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-09-11 12:53:17 +00:00
Siddharth Balyan
0591da2ba6 Guided first launch: review fixes from #107985 and the free-tier chip badge (NS-848, NS-855) (#108211)
* fix(desktop): centralize guide handoff receipt reads

Resolve the guide receipt key and value together in setup-profile. Use the helper at all four read sites so connection scoping follows one implementation.

* fix(desktop): recover from unreadable handoff receipts

Memoize receipt reads and show Retry only for the error phase. Quarantine corrupt data before retrying, and resolve the guide identity when the failed request did not retain it so a fresh build can start.

Cover preservation of corrupt data and removal from the active receipt key with an invariant test.

* fix(desktop): validate persisted onboarding phases from one list

Derive OnboardingPhase and persisted-value validation from the same phase list so future phases survive relaunch. Verify every persisted phase reloads and an unknown value falls back to idle.

* fix(desktop): share window centering arithmetic

Extract centeredBounds and use it for onboarding boot and window growth. Keep the existing work-area clamps and coordinate rounding unchanged.

* fix(desktop): compute progress steps inline

Remove the ineffective ProgressCard memo because streaming flushes replace the messages array. Keep the same transcript scan and rendered steps.

* fix(desktop): center the free-tier status chip detail

Wrap the model label and sign-in badge in an inline flex span with a shared gap. This centers the badge beside the model text without changing other status-bar details.

* fix(desktop): derive the guide receipt key in one place

The Retry path spelled the key derivation out again because the read helper throws on a corrupt receipt before it can return the key. A separate guideHandoffReceiptKey serves both the reader and the quarantine, so the derivation has one home again.

* fix(desktop): keep the free-tier badge at its intended leading

Badge declares leading-none, but the class merger drops it behind the size variant's font-size class, so the badge inherits a 1.5 leading and renders 16px tall next to an 11px label. That height, not the inline alignment, is what read as a detached badge. Restating leading-none on the chip's badge brings it to 11.6px, inside the label's cap height. The Badge component itself is left alone; every other badge in the app has the same dropped leading and that is a separate decision.
2026-09-11 12:46:48 +00:00
brooklyn!
dee30d123d fix(desktop): scope approval hints and omit zero message counts 2026-09-11 07:38:41 -05:00
brooklyn!
b08a26791f fix(desktop): focus opened sessions and restore closed tab positions 2026-09-11 07:38:41 -05:00
brooklyn!
22b4b49aa7 fix(desktop): preserve composer selection across model picking 2026-09-11 07:38:41 -05:00
brooklyn!
e6aa2e9fe4 fix(desktop): satisfy Electron permission type check 2026-09-11 07:37:59 -05:00
YuhGuan
244a42f636 fix(desktop): media-range review follow-ups — ignore multi-range as a whole, fstat the handle being streamed, 404 for missing files
- Multi-range requests (any comma) now fall back to a full 200 instead of silently serving only
  the first part (RFC 7233 permits ignoring Range); documented + pinned by a test.
- Open the file first and fstat that handle, then stream from the same FileHandle, so
  Content-Length and the bytes delivered come from one open file (no stat/stream race).
  Handing the FileHandle (not the raw fd) to createReadStream avoids a double close.
- ENOENT/ENOTDIR return a 404 Response instead of rejecting; covered by a test.
2026-09-11 07:37:59 -05:00
Youssef
360c1ee836 fix(desktop): allow HTML5 video fullscreen through permission handlers
The custom setPermissionCheckHandler only allowed media/audioCapture/
videoCapture, which made Electron deny the 'automatic-fullscreen'
permission consulted during HTML5 video requestFullscreen(). The
request handler's isMediaCapturePermission() also returned false for
'fullscreen'. Result: the native fullscreen button on <video controls>
in chat silently did nothing.

Allow 'fullscreen' + 'automatic-fullscreen' in both handlers.

Verified with a minimal Electron repro using Hermes' exact handlers:
requestFullscreen() failed with 'TypeError: Permissions check failed'
before; works after. User-verified in the packaged desktop app.
2026-09-11 07:37:59 -05:00
brooklyn!
f36b6b0ce6 fix(desktop): cancel bootstrap manifest work during quit 2026-09-11 07:33:17 -05:00
brooklyn!
7b9808e43b fix(desktop): complete quit through one bounded teardown barrier
Let settled remote sessions continue their first quit. Fence late local starts and join existing local and SSH drains without cancelling managed update recovery.

Co-authored-by: Gille <4317663+helix4u@users.noreply.github.com>

Co-authored-by: ChanPark03 <parkchan0302@gmail.com>
2026-09-11 07:33:17 -05:00
brooklyn!
8607d6a52a fix(desktop): retain and cancel owned backend lifecycle work
Track pending starts, pre-claim children, and teardown removed from routing. Bound cleanup and cancel setup/update waits while preserving settled remote descriptors.

Co-authored-by: Gille <4317663+helix4u@users.noreply.github.com>

Co-authored-by: ChanPark03 <parkchan0302@gmail.com>
2026-09-11 07:33:17 -05:00
brooklyn!
556d19c2d5 fix(desktop): preserve refresh errors in media and verify auth recovery over HTTP
Co-authored-by: Sora-bluesky <sora.bluesky.dev@gmail.com>

Co-authored-by: Zeus-Deus <github.commits@widow.cc>
2026-09-11 07:32:34 -05:00
brooklyn!
22751c8fd9 fix(desktop): preserve remote auth through refresh failures and login races
Salvage native refresh coordination and cookie fallback without losing forced bearer rotation or replaying REST mutations.

Co-authored-by: Sora-bluesky <sora.bluesky.dev@gmail.com>

Co-authored-by: Zeus-Deus <github.commits@widow.cc>
2026-09-11 07:32:34 -05:00
xxxigm
5e9157658a fix(desktop): stage group-chat PDFs through file.attach
Direct chat already uploads PDFs into the session workspace. Group turns
called pdf.attach instead, which needs pdftoppm and swallowed failures, so
bots saw the filename and no file. Stage PDFs the same way as other files,
and name a failed attach in that member's prompt.
2026-09-11 07:30:43 -05:00
xxxigm
8184051be7 test(bot-mode): pin group PDF staging to the 1:1 file.attach contract
Group PDFs currently only hit pdf.attach, so a member prompt can name the
file while the session workspace never receives it. These tests require the
same file.attach + @file: ref path 1:1 chat uses, and a named failure when
that staging throws.
2026-09-11 07:30:43 -05:00
brooklyn!
a3190625c0 fix(desktop): refresh missing roster sources after recovery
Queue a forced follow-up when Test overlaps an older enumeration; retain bounded caching for incidental focus events.

Co-authored-by: FalconOrtiz <falcon.ortiz11@gmail.com>
2026-09-11 07:14:55 -05:00
brooklyn!
10555c287b fix(desktop): scope primary auth recovery to its foreground
Ignore startup snapshots superseded by newer progress or an open socket.

Co-authored-by: Carl Taylor <carl@carltaylor.com.au>
2026-09-11 07:14:55 -05:00
brooklyn!
8d092c2f71 fix(desktop): validate remote OAuth through ticket minting
Handle truncated OAuth responses and keep confirmed auth recovery stable. Preserve the current attempt guard before latching failures; the older pre-guard latch proposal is not carried forward.

Co-authored-by: xxxigm <tuancanhnguyen706@gmail.com>
Co-authored-by: FalconOrtiz <falcon.ortiz11@gmail.com>
Co-authored-by: Ugo Enyioha <ugo.enyioha@outlook.com>
Co-authored-by: Bartok9 <259807879+Bartok9@users.noreply.github.com>
2026-09-11 07:14:55 -05:00
brooklyn!
8429a54bac fix(desktop): track the exact group member through presence and stop
Keep a runtime turn descriptor, feed its roster key into row mood and activity filtering, and release only the completing invocation’s presence. Stop uses the captured owner rather than a name lookup.

Co-authored-by: Tuna Dev <tuancookiez@gmail.com>
2026-09-11 07:09:01 -05:00
brooklyn!
6fa8518b44 fix(desktop): reject unsupported group slash commands before delivery
Preserve drafts and attachments through the existing nothing-sent contract. Port the command-shaped guard to the shared send boundary with behavioral coverage and localized feedback.

Co-authored-by: ClintonEmok <54935030+ClintonEmok@users.noreply.github.com>
2026-09-11 07:09:01 -05:00
Adolanium
20f7ef4df5 fix(desktop): yield the plugin titlebar band only to mounted chrome
#107239 hid the app's fixed titlebar clusters on every contributed full
page, so a plugin route that mounts no titleBar.* content got a bare
strip: sidebar toggle, settings gear, layout editor, HUD, flip and
right-sidebar controls all unmounted, and titleBar.tools items were
dropped with no opt-out on RouteContribution.

The band now yields only while the page actually projects chrome into
it. titleBar.* contributions are mount-scoped, so the presence check
follows the page; a chrome-owning page also keeps its titleBar.tools
items in the band.
2026-09-11 06:37:12 -05:00
brooklyn!
433d40686d fix(desktop): republish composer clearance after an effect replay
useComposerMetrics dedupes its --composer-measured-height writes against
a "last published bucket" ref. The unmount cleanup cleared the surface
vars but left that ref alone, so after a non-final unmount (StrictMode
replay, Suspense hide) the re-mount measured the same dock, saw an
unchanged bucket, and never wrote the var back. The thread then read
the :root estimate (~62px) under a dock that could be 200px tall, and
the status stack covered the last turn until a real resize fired.

Reset the bucket refs in the same cleanup that clears the vars. Adds a
StrictMode regression test that fails on the old code.
2026-09-11 06:23:44 -05:00
brooklyn!
aa05e5c0f4 fix(desktop): resolve side ownership before workspace registration 2026-09-11 06:12:12 -05:00
brooklyn!
6cc51b407c fix(desktop): keep sidebar tabs and restore controls clear of titlebar chrome 2026-09-11 06:12:12 -05:00
brooklyn!
477194879c fix(desktop): recover minimized sidebars from their existing controls
Co-authored-by: wukangcheng1994 <160389295+wukangcheng1994@users.noreply.github.com>
2026-09-11 06:12:12 -05:00
hermes-seaeye[bot]
8c74118c4a fmt(js): npm run fix on merge (#108127)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-09-11 10:23:35 +00:00
Siddharth Balyan
b8e8639445 Guided first launch: smaller code and review fixes over PR B1 (NS-848, PR B2) (#107985)
* refactor(desktop): compress intro reveal

Remove the unused inline cinematic fallback and its skip callback plumbing now that the native window owns playback. Keep native timing and exit behavior unchanged, colocate the spinner with text effects, and document the current launch and handoff contract.

Area delta against B1: 47 additions, 53 deletions, net -6 lines across six files. Most fork verdicts were already applied in B1.

* refactor(desktop): compress guided chat surface

Remove random greeting variants and retain one existing opener per locale,
while preserving the banked greeting and machine-name suggestion. Trim
assembly commentary while keeping the reasons for its layout invariants.

Move solo-boot and window-growth IPC into a topical Electron sibling so
onboarding handlers no longer grow main.ts. Preserve sender gating,
reveal ordering and window geometry.

* refactor(desktop): compress onboarding handoff

Split welcome-chat kickoff from durable handoff effects and wire each
hook directly. Remove duplicated option types, a redundant readiness
comparison, nullable receipt-key state and stale prose while preserving
B1 routing and recovery.

* refactor(desktop): compress guided chat back half

Remove the duplicate handoff completion key and its reader/writer helpers.
Use the onboarding phase record for replay guards and settled cards while
keeping accepted receipts as the completion boundary.

Preserve the signpost and plugin plan under ruling 5.

* refactor(desktop): compress stores and transcript integration

Remove unused machine reset and untargeted host composer submission. Trim machine and presence commentary while preserving their live consumers. Wire reasoning through the existing scratchpad surface and memoise progress history without mutating it.

Keep parser and connector rendering under rulings 4 and 5. Area delta: 36 insertions, 101 deletions; net -65 lines.

* fix(desktop): keep skipped onboarding apart from a completed handoff

Make skipGuide() persist skipped and let beginOnboardingHandoff accept
guided or skipped. requestSetupHandoff and HandoffCard derive completion
from the accepted receipt.

The latch merge conflated skipping the guide with starting the first
build. A later handoff therefore claimed "was started" without creating
a session. Preserve skipping as its own terminal phase so a later
handoff can create the build and reach done only after acceptance.

* refactor(desktop): B2 review notes

Correct the layout-growth comment in assembly.ts: growing to preserve
the chat size balloons the window. Restore the WHY clauses in
onboarding-handoff.ts and onboarding-kickoff.ts for pending title metadata
on older backends and the caller's requestGateway reading the create pin.

Move guideSourceConnectionId beside $setupSession in setup-profile.ts
and derive each hook's option types from useSessionActions, so kickoff
no longer imports the heavier handoff leg.

Move $handoffError and retrySetupHandoff beside $setupHandoff in
setup-profile.ts and update the card and handoff hook importers.
This removes the setup-profile/handoff-receipt cycle and leaves receipt
persistence dependent only on storage and its receipt type.

* fix(desktop): derive the first-build receipt key one way

Use guideSourceConnectionId(guide.storedId) for the save and request
receipt keys, matching resume and HandoffCard. Keep guide.connectionId
for RPC routing and preserve the receipt key's string format.

At boot the resume path only knows the guide's stored id. When no owner
hint exists but an active gateway connection does, keying writes by the
resolver's ambient route hides the accepted receipt from resume and
leaves the card on Opening. One derivation lets every path find the same
receipt without changing where the build request is sent.

* feat(desktop): intro type at 150% for legibility

Set the intro window's root font size to 150%. Every measure in the intro
is in rem, so the chat card, its rows, bubbles and gaps scale together.
The brand close uses viewport units; its wordmark and tagline are scaled
by hand to match (6.8 to 10.2 vmin, 1.35 to 2 vmin). The hero card's
width cap rises from 900 to 1350 px so lines keep their length on a
large display; its minimum width is unchanged so the three-column stage
still fits a laptop.

The intro fills a display the user sits back from, and at the app's 16 px
root its text read too small on a large monitor (director ruling).

* fix(desktop): status bar keeps one fill under glass; free-tier chip reads Nous, model, Sign in

Under the glass appearance in sidebar scope, the body paints a hard stop
at the rail's edge (glass mix left, opaque chrome right) and the status
bar was transparent, so the seam ran through the bar and cut whichever
item sat on it: in the 886 px guided window, the free-tier chip. The bar
now belongs to the opaque content column across the full width, the way
Finder's does; window scope has no seam and keeps the transparent bar.

The chip itself read "Nous · free tier · nous/welcome" with the Sign in
badge touching the label. It now reads "Nous", the model id small and
monospace, then a solid Sign in badge set off by a gap; the full
"Nous · model" string moves to the tooltip. "Free tier" is no longer
said in the bar (director ruling).
2026-09-11 15:45:48 +05:30
Siddharth Balyan
0e927c914d Guided first launch behind HERMES_GUEST_ONBOARDING: intro, guided chat, first task in default (NS-848, PR B1) (#107958)
* feat(desktop): port guided onboarding substrate

Add seeded session creation, transcript directives, profile routing, and the shared window and pane primitives needed by the guided flow. Keep later-step mounts deferred and exclude provider selection and retry machinery.

* refactor(desktop): anti-slop cleanup for substrate

Assemble seed parameters in the existing create helper and use the owning transcript attribute type. Read the guaranteed gateway and connection contracts directly to remove runtime type probes and unchecked assertions.

* test(desktop): create-overrides invariants

Verify that reasoning and title overrides do not select a provider or model. Empty overrides and seeds add no parameters.

* feat(desktop): port first-run cinematic window

Play the cinematic behind the guest onboarding launch flag using bundled Collapse and JetBrains Mono. Give the native window its own controller and restore the app on skip, renderer deadman or native watchdog.

Drop the perf scenario because it depends on the removed replay hook. Guided chat kickoff and app-shell gate wiring remain with their later steps.

* refactor(desktop): anti-slop cleanup for cinematic

Preserve audio and canvas behavior through named types and inferred results. Split the viewport node and frame drawing to keep control flow bounded. Cut comments that only repeat the code.

* feat(desktop): add onboarding gate and answers stores

Track cinematic, guided chat, handoff and completion in one phase record. Queue the guide after the intro and share pending kickoff work between callers.

Keep existing saved answers while dropping retired preferences. Leave intro seen-state ownership with the cinematic store.

* feat(desktop): port guided onboarding chat

Add guided setup cards, runbooks, machine context, and onboarding presence. Connect transcript rendering and first-build progress to the desktop behind the onboarding flag. Leave session kickoff and handoff execution for the next step.

* refactor(desktop): anti-slop cleanup for guided chat

Keep directive and layout lookups typed. Remove unsafe test casts and isolate onboarding transcript calculations without changing the flow.

* feat(desktop): connect guided onboarding to durable first-build handoff

Start the guide only after its profile backend confirms bootstrap readiness. Seed or adopt the welcome chat, then transfer the first build to default with a durable receipt and explicit retry.

Wire cinematic completion, screen stand-down, layout growth and progress check-ins. Save agreed preferences before creating the build and release prompt slots after storage refusal.

* refactor(desktop): anti-slop cleanup for onboarding handoff

Reuse the gateway request and error contracts. Isolate guide adoption and snapshot validation while preserving receipt recovery and reasoning overrides.

Validate persisted receipt fields at the JSON boundary without coercion. Keep corrupt identities rejected and retain only the permitted test mocks.

* fix(desktop): guided chat review fixes

Wire the native machine probe so guided setup can suggest a name and offer the right first task. Restore the comments that explain the flow boundaries.

The directive registration uses the launch flag to preserve ordinary chat. Ruling 6 folds active.ts into assembly to keep activity ownership together and removes the second greeting source so the seeded and visible greetings agree.

* fix(desktop): handoff review fixes

Probe the guide backend before switching profiles so a readiness refusal keeps classic onboarding on the current backend.

Restore list-valued personalization coverage and routing rationale. Remove the obsolete setup status fixture.

* chore(desktop): onboarding script cull and rehearsal recipe

Document a temporary-state rehearsal using the existing onboarding flag and optional portal stand-in. Keep the main scripts unchanged and retain window growth for the guided chat.

* fix(connectors): reject incomplete catalog responses

* feat(gateway): scope connector controls to the owning session

* feat(desktop): connect apps through native session-owned controls

* feat(desktop): gate connector cards and enable free-tier access

Use the launch flag before mounting connector controls so classic transcripts add no status requests. Allow existing free-tier identities through the read-only tool gateway gate and test the owning-profile RPC path with A’s launch gate. Keep authorization links out of previews.

* style(desktop): format connector translations

Apply Prettier to the connector copy blocks while preserving upstream translations and free-tier wording.

* refactor(desktop): anti-slop cleanup for connector card

Use the transcript JSON contract and concrete RPC parameters. Preserve malformed-value filtering at one string boundary and make the fixture and row types explicit. Keep connector execution and cancellation behavior unchanged.

* feat(desktop): detect initial language from the OS

Use the native machine locale when no supported language is saved. Preserve explicit choices and leave inferred languages out of config.

* refactor(desktop): anti-slop cleanup for initial locale detection

Keep unvalidated config values at the existing validation boundary. Pass no saved choice after that boundary has ruled it out, preserving locale precedence.

* test(desktop): onboarding port test set

Make native window tests reject duplicate IPC handlers and isolate disabled onboarding. Assert the active gate mock when onboarding re-enables.

Keep the test set limited to behavior carried by the port.

* fix(desktop): recover failed guide kickoff and reveal once

The review found that a failed guide create stranded the solo shell and draft profile, and solo boot faded an already visible window a second time. Restore the prior route and layout, release onboarding through its existing phase record, and surface create failures. Let the film own the reveal while solo boot animates the visible resize.

* fix(desktop): preserve transcript ownership across cards and handoff

The review reproduced answers submitted to the focused chat, repeated questions disabled across sessions, handoff recovery using foreground identity, and mount-dependent progress history. Target each card’s own composer, scope settlement to its message and session, carry the issuing guide through handoff, and derive progress from its transcript with streaming activity. Reuse the existing owner ladder for exact and profile-only routes.

* fix(gateway): preserve connector ownership with profile routing

The review found that shared-primary profile metadata was rejected before connector dispatch, while desktop controls treated a missing registry id as missing ownership. Accept profile only as routing metadata and keep the live transport as authorization. Resolve card ownership through the existing exact/profile ladder, retaining ambient routing only for the single-backend case.

* fix(desktop): resolve plugin roots and gate the Basic layout

The review found that the first plugin build was seeded with a different installation’s fixed path, and the director ruled that flag-off layouts must match main. Resolve the running desktop’s plugin root before seeding a plugin build and register Basic only when onboarding is enabled. Keep the runbook wording and the ordinary four layout presets intact.

* fix(desktop): clear review-fix slop findings

The slop gate flagged an undocumented layout-data assertion and unknown-return types in the new test selectors. Record the layout registry invariant and preserve each selector’s return type. The only remaining production finding is the accepted connector-tools baseline.

* fix(desktop): detect the OS language on a fresh install

The review found that the merged English config default prevented the
desktop from probing the OS language on a fresh install. Add an opt-in
saved-values read so an absent choice remains distinct from saved English.

Preserve default-valued English only for explicit language saves; unrelated
settings saves must not turn a merged default into a language choice.
Older backends ignore the new query options and keep returning merged
English, preserving their existing desktop behavior.

* test(desktop): make the flag-off layout registry test deterministic

The flag-off test awaited the full controller import, pulling in the UI
graph and installing application watchers just to read layout presets.
That import took 9.5 seconds locally and timed out in the director's run.

Move the existing trees and registration into a small layout-presets
module. Production and the synchronous test use the same flag-gated
registration, without starting the controller in the test. Keep the real
registry invariant and dispose the test's contributions after completion.

* fix(desktop): keep the transcript parser and ::ask behind the onboarding flag

Register the guided chat's question card only with onboarding enabled.
Restore main's whole-paragraph parser and contribution rendering when the
flag is off, including its streaming prose behavior. Keep segmentation for
the guided flow until B4 decides the parser's wider use.

Restore main's two parser test files so its existing product and plugin
contracts remain the flag-off check.

* test: drop the onboarding and connector tests pending a later ticket

Apply the director's ruling to remove B1's added test files and restore
main's existing suites. Keep only the gateway route-reader mock contract
that main's profile tests need against the shipped activation behavior;
their cases and assertions stay intact.

The flow's shape is not settled and B3/B4 rewrite it. The connector layer
will also be reworked. The live CDP run is the flow check until a follow-up
ticket brings tests back.

---------

Co-authored-by: brooklyn! <brooklyn.bb.nicholson@gmail.com>
2026-09-11 15:45:43 +05:30
Teknium
5d2d5e906d fix(tests): banner ssh-fastpath tests patch the seam production reads (_github_branch_tip)
Since 338bf9ea9a _check_via_local_git reads _github_branch_tip directly; patching _upstream_main_sha intercepted nothing, so on the CI runner the check made a live GitHub call and behind stayed None. From #107835 (banner half; the anon_auth half landed via #107822).
2026-09-11 15:30:09 +05:30
kshitijk4poor
1827a8584e refactor(desktop): name the in-memory tail wipe by what it clears
Polish after #107993: `clearAllTranscriptTails` sat next to the cache's
`clearTranscriptTails` differing by one word that did not encode which
store each empties; rename it `clearTranscriptTailPaging` and keep the
WHY at the one call site instead of repeating it in the JSDoc. The
gateway-switch test resets paging state in afterEach through the helper
(covers the LRU order too) rather than an inline atom reset that a
failing assertion would skip. Drop a duplicated "capture before await"
sentence in getLatestSessionMessages.
2026-09-11 15:29:35 +05:30
Teknium
cbd03e6e4c fix(gateway): secondary-profile adapters no longer inherit the default's allow-all / allowlists
Under gateway.multiplex_profiles, os.environ holds the DEFAULT profile's .env. Several
adapter-owned authorization gates still read GATEWAY_ALLOW_ALL_USERS, GATEWAY_ALLOWED_USERS
or their platform allowlist/allow-all raw from os.environ, so the default profile opting
into open access opened every secondary email/QQ/WhatsApp/Matrix/Teams/Slack/LINE/DingTalk
bot to any sender (email additionally skipped From: authentication), the default's Matrix
allowlist decided who may approve tool calls on a secondary bot, and a secondary that
opted in only in its own .env was silently deny-all.

Every such read now goes through the adapter's existing module-local scoped reader
(gateway.platforms._shared.get_scoped_secret / matrix _startup_env_secret): profile
scope first, scoped miss = default, never os.environ; the unscoped default-profile and
single-profile paths keep the environ read, where it IS the profile's own value.

Sites: email _allow_all_senders/_allowlist_in_effect; qqbot _open_dm_opted_in;
whatsapp_common _open_dm_opted_in/_live_dm_allow_from; teams _card_action_denied;
matrix _is_authorized_user, MATRIX_ALLOWED_USERS, MATRIX_IGNORE_USER_PATTERNS,
_extra_csv_set (allowed/free-response rooms); slack _slack_allow_bots/_slack_api_human_users;
line _truthy_env/allowlist (allow-all, user/group/room allowlists); dingtalk _extra_get
(allowed_users/chats, free-response chats, require_mention).

Live repro (temp HERMES_HOME, multiplex on, default env GATEWAY_ALLOW_ALL_USERS=true,
secondary scope without opt-in): EmailAdapter._allow_all_senders() True -> False,
QQAdapter._open_dm_opted_in() True -> False, Matrix _is_authorized_user('@stranger')
True -> False, Teams card action allowed -> denied.

Co-authored-by: Drexuxux <drexux0@gmail.com>
Co-authored-by: MoonsvnLyn <FirmamentalSpring@users.noreply.github.com>
Co-authored-by: svector-anu <anuoluwakolapo94@gmail.com>
Co-authored-by: babatorik <durgun.ismail@gmail.com>
Co-authored-by: salch-cred <salch-cred@users.noreply.github.com>
2026-09-11 02:24:55 -07:00
Teknium
94fb74fa97 docs(bot-mode): explain Warm Bot Backends, idle reaping, and slot waits
Fleet users read "Hermes backend for profile X exited (1)" as a crash and
raise Warm Bot Backends past their profile count to make it stop. Name the
setting, the defaults, what the exit line means, and which operations take
a slot so the knob is tuned for active bots instead of total profiles.
2026-09-11 02:23:29 -07:00
fangliquanflq
ad6fdd4f1c fix(desktop): route session reads through primary backend 2026-09-11 02:23:29 -07:00
Teknium
fc3d60af09 feat(compression): provider-scoped model_thresholds keys ("<provider>:<substr>")
A bare `astra: 0.85` in compression.model_thresholds was written for the Codex
OAuth route, where Astra is capped at 272K and 50% would compact at ~136K. The
key is substring-matched on the model name alone, so it also fired on
openai/gpt-6-astra via OpenRouter and Nous, where the window is 1.1M: the user's
0.5 global threshold was silently replaced by 0.85 and the session sat at 620K
(~59%) without compacting.

Keys may now carry a provider prefix: `"openai-codex:astra": 0.85` applies only
when the session's provider is openai-codex; bare keys keep their route-agnostic
behaviour. Ranking is by model-substring length with scope as the tie-break, so
`astra-900k` still outranks `openai-codex:astra` for the 900K picker. The
provider flows through ContextCompressor (ctor + update_model), the ContextEngine
base class and the TUI hot-reload path, so a /model switch between routes
re-scopes the override.
2026-09-11 02:07:48 -07:00
Teknium
9ce7547faf fix(update): bound network git in hermes update and prune shallow grafts on apply
- _git_run(network=True) now carries a 300s timeout; a dead-stalled fetch
  (HTTP/2 to GitHub on some networks, black-holed proxy) becomes a failed run
  whose stderr names the stall instead of an update pinned on
  'Fetching updates...' forever (#93759, #95777). Local git stays unbounded.
- The apply path prunes stale .git/shallow grafts alongside its existing
  lock/tmp_pack cleanup, so installs that already accumulated grafts from
  past depth-1 checks (#105951: 57 entries) heal on their next update, not
  only on --check.
2026-09-11 02:06:32 -07:00
liuhao1024
5bfa389e8a fix(cli): prune stale shallow grafts left by depth-1 update checks (#105951)
Every 'git fetch --depth 1' in 'hermes update --check' (and the past
banner passive checks, before #107648 moved them to the GitHub API)
appends the fetched tip to .git/shallow as a new graft and git never
removes the previous one, so a long-lived shallow installer checkout
accumulates one graft per check (57 observed). The stale grafts break
merge-base and push 'hermes update' into the orphan-divergence reset
path with a rescue ref on every run.

prune_stale_shallow_grafts() now runs after each successful depth-1
fetch in 'hermes update --check' and clears the grafts already
accumulated by past checks: it keeps only the boundaries still
protecting referenced tips (HEAD, FETCH_HEAD, every ref tip) and
atomically rewrites .git/shallow, restoring the original file if the
trimmed set breaks history walking. The dropped commits are already
unreachable; their objects are left for git gc.

Rebased onto main after #107648: the banner.py hook is dropped (the
passive check no longer git-fetches); the update --check prune and the
cleanup of already-accumulated grafts are kept.

(cherry picked from commit 6174837fc5b9f4cc3d4d46dc1b2d9a2f6b83c120)
2026-09-11 02:06:32 -07:00
Teknium
66a13703b3 fix(desktop): update-check failures name the real cause instead of 'couldn't reach the update server'
A GitHub outage, a rate limit, a corporate proxy intercepting TLS and a DNS
failure all rendered as the same generic line, so #105855 read as a Hermes
bug during a run of GitHub incidents. The main process now classifies the
failure (HTTP status incl. 403/429 rate-limit and 5xx outage wording, DNS,
timeout, connection refused/reset, TLS) into one actionable sentence; the
overlay shows it under the title and About appends it to the status line.
2026-09-11 02:06:32 -07:00