Commit Graph

298 Commits

Author SHA1 Message Date
Evgenii f9d64b9a9d fix(cron): add reliable trigger feedback in Web and Desktop clients
- Shared per-job trigger controller (apps/shared) coalesces duplicate
  clicks for the same profile+job inside a mounted client while letting
  unrelated jobs run independently; the backend durable claim remains
  authoritative across windows/processes.
- Two-phase feedback everywhere: the action stays disabled/spinning
  while the request is in flight and the terminal success/error is
  reported once, after the HTTP response — no premature success toast
  (Web), matching the Desktop info notification.
- Desktop keeps the 24h trigger timeout for the synchronous long
  operation and fences stale profile/list responses and unmounted
  surfaces; the sidebar trigger button shows a spinner while busy.
2026-08-14 20:46:50 -07:00
Teknium 5821691642 test(dashboard): extend ChatPage Terminal fake with onScroll/buffer/scrollToBottom
The resume follow-scroll fix subscribes term.onScroll and reads
term.buffer.active; the ChatPage test fake predates both and crashed
the suite with 'term.onScroll is not a function' (4 unhandled errors).
2026-08-14 15:00:05 -07:00
Teknium 7f7ca89255 fix(dashboard): weave resume follow-scroll into sanitizer write path + contributor mapping
Follow-up to the salvaged #59713 commits: main's ChatPage gained the
PTY resume sanitizer + hydration machinery after the PR was authored, so
the write-callback follow-scroll is applied to the sanitized write path
(term.write(rendered, followScroll)) rather than the raw ev.data writes
the original diff targeted. Also adds the contributor email mapping.
2026-08-14 15:00:05 -07:00
VBS2004 4406d8ca6a test(dashboard): cover resume-scroll stick-to-bottom logic
Extract the resume-scroll decision into lib/pty-scroll (isViewportPinnedToBottom, shouldFollowPtyOutput) and add focused vitest coverage, per review on #59591.
2026-08-14 15:00:05 -07:00
VBS2004 82e05b2040 fix(dashboard): scroll terminal to bottom when resuming session
Fixes #59591
2026-08-14 15:00:05 -07:00
Buff Pesos 56f1afc834 feat(dashboard-auth): extend RFC 8252 native sign-in to password providers (system-browser autofill) (#75808)
* feat(dashboard-auth): extend RFC 8252 native sign-in to password providers

The desktop app runs password sign-in for gated gateways in an embedded
Electron BrowserWindow, where OS password managers (macOS Passwords /
iCloud Keychain autofill) cannot reach the form — Chromium-in-Electron
has no bridge to them, so users retype credentials by hand even though
the /login form already carries the right autocomplete attributes.

The existing RFC 8252 native flow (system browser + loopback + PKCE)
solves exactly this for OAuth providers, but was explicitly disabled for
password providers on the grounds that they have "no IDP round trip to
broker". The brokering is still worth having: it moves the credential
form into the system browser, where password-manager autofill just works.

Gateway-only change; the desktop needs no changes (runNativeLogin is
already page-agnostic), and older desktop builds pick the capability up
automatically once the gateway advertises it:

* /auth/native/authorize now accepts a supports_password provider:
  register the pending broker authorization as usual, then 302 the
  system browser to the interactive /login form with the opaque
  broker_state in the gateway's PKCE cookie (the same server-controlled
  channel the OAuth branch uses) instead of an IDP redirect.
* /auth/password-login: when the server-set PKCE cookie carries a
  broker handle, a successful credential check completes the pending
  authorization exactly like the /auth/callback native branch — mint
  the one-time loopback code, return the loopback redirect (validated
  loopback-only at authorize time) as `next`, clear the PKCE cookie,
  and set NO session cookies. A lapsed broker is a clean 400 telling
  the user to restart sign-in; a failed credential attempt leaves the
  pending entry intact so the user can retype.
* /api/status now advertises "native_pkce" whenever any interactive
  session provider is registered (previously only for non-password
  providers), so the desktop selects the system-browser strategy for
  password-only gateways.

Security posture is unchanged from the existing flow: loopback-literal
redirect_uri enforcement, PKCE S256 binding, single-use short-TTL codes,
constant-time comparison, and the same rate limiter on password attempts.

Tests: full authorize → /login → password-login → loopback → token →
bearer round trip, wrong-password keeps the pending entry, lapsed broker
→ 400, no-broker browser login keeps minting cookies, and the /api/status
advertisement for password-only gateways.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(dashboard-auth): bind native password completion to the authorize-time provider

Review follow-ups for #75808:

* /auth/password-login now enforces that body.provider matches the
  provider recorded in the server-set PKCE cookie by
  /auth/native/authorize before completing a pending native
  authorization. /login renders a form for every session provider, so
  without this a native flow started for provider A could be completed
  with provider B's credentials, binding B's session into A's pending
  entry. The mismatch is rejected BEFORE credential verification (no
  session minted, no oracle) and preserves both the pending entry and
  the cookie, so the user can still submit the correct provider's form.
  Covered by a two-password-provider E2E regression test.

* Update the two docs spots that still said password-only providers do
  not advertise native_pkce (website desktop-native-signin guide and the
  auth_flows type comment in web/src/lib/api.ts).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* chore: map contributor email for #75808 (buffpesos)

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Brooklyn Nicholson <brooklyn.bb.nicholson@gmail.com>
2026-08-14 21:39:01 +00:00
Shannon Sands 6977d21fa7 feat(gateway): disk-usage telemetry + dashboard disk-pressure banner (NS-656)
Extends the NS-656 memory-pressure surface to cover disk exhaustion
(OOF-2 / OOF-107 lineage: agents fill their data volume — SQLite writes
fail, sessions stop persisting — while every dashboard looks healthy).

- gateway/disk_status.py (new): collect_disk_status() samples
  shutil.disk_usage(HERMES_HOME) and classifies pressure
  (critical: <256 MB free or >=95% used; elevated: <512 MB free).
  Never raises — degrades to pressure="unknown" with null telemetry,
  same contract as collect_memory_status().
- /api/status: sibling `disk` block next to `memory`, advisory only —
  not folded into component/overall health.
- web: DiskPressureStatus type; MemoryPressureBanner generalized to a
  resource banner with worst-first triggers (disk critical > memory
  critical > OOM restart > disk elevated > memory elevated) and
  cascading dismissals — hiding the top trigger surfaces the next one
  instead of silencing everything. All dismissals stay boot_id-scoped.
- i18n: diskCriticalBanner / diskElevatedBanner (en, optional fields
  with English fallback per existing pattern).

Tests: gateway/test_disk_status.py (14), web_server disk-block
presence/degradation, banner disk trigger/priority/dismissal-cascade
suite (21 total).
2026-08-13 20:30:12 -07:00
Shannon Sands f5a26b1575 fix(web): scope every memory-banner dismissal to boot_id (NS-656)
Review edge case: OOM dismissal was boot-keyed but live critical/elevated
dismissals were keyed only by severity. Dismiss critical, gateway reboots,
next poll still critical with no observed "ok" in between — the new
incident stayed hidden, and masked the OOM notice too, since critical
takes precedence.

Every dismissal key now embeds boot_id, so a gateway restart invalidates
prior dismissals of any kind. Within a boot, semantics are unchanged:
severity-scoped masking, escalation re-opens, confirmed "ok" recovery
clears live dismissals, "unknown" clears nothing. Missing boot_id
(pre-NS-656 image) degrades to a shared per-severity bucket as before.
2026-08-13 20:30:12 -07:00
Shannon Sands ba5dc00bb2 fix(memory-status): review follow-ups — incident-keyed dismissal, honest copy, single mobile offset (NS-656)
Addresses the human review findings on the memory-pressure feature:

* [P2] Dismissal hid later incidents of the same kind. The gateway now
  publishes `boot_id` (the lifecycle sentinel's started_at — changes on
  every gateway life) in the /api/status memory block, and the dashboard
  keys OOM-restart dismissal on it: acknowledging one restart no longer
  mutes the NEXT one (the OOM-loop case this banner exists for). Live
  pressure dismissals now also reset once pressure is demonstrably back
  to "ok" — "unknown" (stale heartbeat) is absence of evidence and
  clears nothing. Dismissal storage moved to a JSON list; old bare-string
  entries fail JSON.parse and degrade to a clean reset.

* [P2] suspected_oom is a heuristic (unclean exit + low-memory final
  heartbeat), not proof the OOM killer acted — banner copy now says
  "restarted unexpectedly, most likely because it ran out of memory"
  instead of stating OOM as fact.

* [P3] Mobile header clearance was applied per-banner (mt-14 on both
  MemoryPressureBanner and ProfileScopeBanner) AND on the content
  (pt-14), double/triple-stacking 56px gaps when banners were visible.
  Replaced with a single h-14 spacer above the banner stack.
2026-08-13 20:30:12 -07:00
Shannon Sands 1745cf3b40 fix(web): rename memory-pressure interface to avoid declaration merge with existing MemoryStatus
api.ts already declares MemoryStatus for the /api/memory providers
endpoint; the NS-656 pressure block reused the name, and TypeScript
declaration merging fused the two shapes — 'tsc -b' in the Docker image
build failed on every test fixture. Renamed to MemoryPressureStatus.
2026-08-13 20:30:12 -07:00
Shannon Sands e11d1ddc7f feat(status): surface memory pressure and suspected-OOM restarts to users (NS-656)
Hosted agents can be OOM-killed hourly while the dashboard and the NAS
agent card both look perfectly healthy — every memory signal the gateway
already produces (heartbeat mem samples, lifecycle-ledger unclean-exit
verdicts, cache-pressure evictions) dies in server-side log files. The
BlueAtlas incident (NS-608) ran for three days like this.

This is the read-side fix:

* New gateway/memory_status.py distills the existing 30s loop heartbeat
  (gateway RSS + system MemAvailable/MemTotal + swap) and the lifecycle
  sentinel into a compact `memory` block: pressure ok/elevated/critical/
  unknown, coarse MB numbers, and last-boot unclean/suspected-OOM flags.
  Pure file reads, no new sampling, no gateway IPC. Stale (>150s) or
  future-dated heartbeats degrade pressure to "unknown" so a dead
  gateway's final gasp can't render a live "critical" banner forever.
  Critical thresholds mirror the ledger's OOM-suspicion heuristics: if a
  level would make a later unclean death "suspected OOM", warn at that
  level while the process is still alive.

* lifecycle_ledger.record_startup now carries prior_unclean_exit /
  prior_suspected_oom onto the reclaimed sentinel — previously the
  verdict survived only in append-only diag prose. Flags age out on the
  next sentinel rewrite (scoped to the life after the crash).

* /api/status serves the block (profile-aware, executor-offloaded,
  fail-safe to pressure=unknown). Deliberately NOT folded into
  components/overall: memory pressure is advisory, and flipping overall
  to "degraded" on it would page NAS's availability sweep for a
  condition the eviction valve is already handling. Public-safety:
  coarse numbers/enums/booleans only — same disclosure class as the
  existing nous_session_valid field, added for the same NAS-sweep
  audience.

* Dashboard: new MemoryPressureBanner (app-shell, next to
  ProfileScopeBanner) with worst-first trigger precedence
  (critical > suspected-OOM restart > elevated), per-trigger
  session-scoped dismissal, and escalation re-opening past a dismissal.
  i18n keys optional with English fallbacks, matching the
  managingProfileBanner convention.

Tests: gateway/test_memory_status.py (classification bands, staleness,
clock skew, corrupt files, bool-is-not-int), lifecycle sentinel
carry-forward, /api/status contract (block always present, collector
crash degrades instead of 500), and 7 banner component tests.

NAS-side ingestion (agent-card notice + memory-tier upsell) ships
separately.

Refs NS-656; context: NS-608, NS-657, OOF-77.
2026-08-13 20:30:12 -07:00
Austin Pickett 2382f50f53 fix(dashboard): bound WS ticket minting on the events + PTY sockets (supersedes #81931) (#81978)
* fix(dashboard): retry stalled events feed reconnects

* fix(dashboard): bound the PTY ticket request before the socket exists

ChatPage's connect awaits a single-use ticket from `api.buildWsUrl()`
before `new WebSocket()`. That request produces no socket, so a
rejection or a hang emits no `close` event and never arms
PTY_CONNECTING_TIMEOUT_MS (set after the socket is constructed). The
tab stranded on "connecting" with `connectInFlightRef` stuck true,
which also suppresses the page-resume reconnect path.

Give the ticket phase its own deadline and route both failure modes
into the existing backoff. A `ticketSuperseded` flag invalidates a late
ticket result so a timed-out attempt cannot open a socket behind the
replacement it scheduled, and cleanup clears the timer on unmount.

`scheduleReconnect` now takes `number | null` so an attempt that died
before any socket existed omits the "(code N)" banner suffix instead of
inventing one.

Same bug class as the events-feed fix in the preceding commit, on the
main chat surface.

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

* test(dashboard): cover the PTY ticket connect deadline

Mirrors the events-feed cases in ChatSidebar.test.tsx: a rejected ticket
retries, a stalled ticket times out and its late resolution cannot open
a superseded socket, and a settled ticket disarms the deadline so
PTY_CONNECTING_TIMEOUT_MS remains the only guard on a wedged handshake
(NS-591 regression).

Both failure cases fail against ChatPage.tsx without the preceding fix.

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

---------

Co-authored-by: Gille <4317663+helix4u@users.noreply.github.com>
2026-08-08 16:01:02 -04:00
kinsolee c750d5354a fix(sessions): prevent oversized transcripts from exhausting memory 2026-08-08 13:36:08 +05:30
Austin Pickett fb402106f8 fix(dashboard): auto-reconnect the events WebSocket with backoff (supersedes #47876, #47921, #24315) (#79524)
* fix(dashboard): add events-feed reconnect policy helpers

Extract the reconnect arithmetic and close-code classification for the
ChatSidebar /api/events socket into a pure module so both can be tested
without a fake WebSocket or a mounted component.

Two decisions live here rather than inline in the effect:

- `shouldRetryEventsClose` — 1000 (normal) and 4401/4403 (auth) are
  terminal; everything else, including 1005/1006 from a killed gateway
  or a dropped network, is retryable.
- `isEventsFeedMessage` — the sidebar's banner is shared with
  `info.credential_warning` and the JSON-RPC sidecar, so a reconnect may
  only clear a message the events feed wrote itself.

Co-authored-by: Ishan Parihar <ishan@supreme-god.dev>
Co-authored-by: Vyre <vyre@ishanparihar.com>
Co-authored-by: eric-senyao <178080753+eric-senyao@users.noreply.github.com>

* fix(dashboard): auto-reconnect the events WebSocket with backoff

The chat sidebar's /api/events subscriber surfaced a static "disconnected"
banner on a transient drop and never retried, so a gateway restart or a
network blip left the feed dead until the user reloaded the page. The feed
drives the live chat title (session.info) and dashboard.new_session_requested,
both of which silently stopped working.

Reconnect with exponential backoff (1s → 2s → 4s → … → 30s cap, 15 attempts
then a terminal banner). Specifically:

- One scheduling path. `close` always follows `error` for a failed socket,
  so scheduling from both — as the superseded PRs did — queues two timers
  and leaks the one that is no longer tracked for cleanup. `scheduleReconnect`
  returns early when a retry is already pending.
- The auth ticket is re-minted per attempt via `buildWsUrl`; tickets are
  single-use with a short TTL, so replaying the first URL would 4401 on the
  second attempt.
- A superseded socket's late close cannot schedule a retry on top of its
  replacement (`isCurrent` generation check).
- A successful open resets the backoff and clears only the events feed's own
  banner, leaving a credential warning or sidecar error visible.
- The pending timer is cleared on unmount, not merely neutered by the
  `unmounting` flag.

Also retires two strings the tools box left behind when it was removed in
47fccc073 (#51737): the banner no longer promises "tool calls may not
appear" and the button reads "reconnect events feed".

Co-authored-by: Ishan Parihar <ishan@supreme-god.dev>
Co-authored-by: Vyre <vyre@ishanparihar.com>
Co-authored-by: eric-senyao <178080753+eric-senyao@users.noreply.github.com>

* test(dashboard): cover the events-feed reconnect bug class

Fake-timer coverage for the behaviors the superseded PRs changed without
tests. Each case was mutation-checked — reverting the corresponding guard
in ChatSidebar.tsx makes exactly that test fail:

- transient close reconnects, and backoff grows 1s → 2s → 4s
- error + close on one socket schedules ONE retry, not two
- a successful open resets the backoff to 1s
- 4401/4403 and a normal 1000 close never retry
- the attempt cap stops the loop instead of retrying forever
- reconnect clears the feed's own banner but not a credential warning
- unmount clears the pending timer (asserted via `vi.getTimerCount()`,
  since the `unmounting` flag alone hides a leaked timer)

Co-authored-by: Ishan Parihar <ishan@supreme-god.dev>
Co-authored-by: Vyre <vyre@ishanparihar.com>
Co-authored-by: eric-senyao <178080753+eric-senyao@users.noreply.github.com>

* fix(dashboard): stop the events feed overwriting a foreign banner

Review catch: `clearEventsBanner` guarded the shared banner but `surface`
did not, so the guard was only half applied. A sidecar error or
`credential_warning` already on screen when the feed dropped was replaced
by "events feed disconnected" — and lost for good, since `error` is that
message's only home and the sidecar does not re-emit.

`surface` now writes only over an empty banner or one of the feed's own
messages. Declining to write does not affect the retry itself; the
reconnect still runs on schedule, it just stays silent while a more
important message holds the banner.

Both directions are covered: a foreign banner survives a drop, and the
reconnect still fires while suppressed.

---------

Co-authored-by: Ishan Parihar <ishan@supreme-god.dev>
Co-authored-by: Vyre <vyre@ishanparihar.com>
Co-authored-by: eric-senyao <178080753+eric-senyao@users.noreply.github.com>
2026-08-05 14:27:04 -06:00
kshitij b8b17b8cee fix: wire HermesConsoleModal WS into stale-token reload guard
Sibling site missed by PR #54022 — /api/console WebSocket in
HermesConsoleModal.tsx has the same buildWsUrl → stale-token → 4401
close path as the PTY and events WebSockets. Without this guard,
opening the console after a dashboard restart shows 'Console closed
(4401). auth: token_mismatch' with no recovery.
2026-08-04 15:26:37 +05:30
kshitij 19e697d9c3 fix: update ChatPage test import for react-router v7
react-router v7 exports MemoryRouter from 'react-router', not
'react-router-dom'. The test was written when the repo still imported
from 'react-router-dom' (4000+ commits ago).
2026-08-04 15:26:37 +05:30
izumi0uu cab8673ea6 fix(dashboard): reload loopback tabs after stale session-token closes
Loopback dashboard tabs now share one one-shot stale-token recovery path across REST 401s, the PTY socket, the structured event socket, and the shared JSON-RPC gateway wrapper. The shared client exposes only an optional close-event interception hook; the dashboard remains responsible for deciding that loopback 4401 means reload.

Constraint: Current main delegates the web gateway to apps/shared JsonRpcGatewayClient, and #54022 review requires a shared-client-compatible close-code hook plus direct ChatSidebar event-socket coverage.
Rejected: Restore the dashboard's old direct WebSocket implementation | stale against the shared JSON-RPC client and would duplicate transport behavior.
Confidence: high
Scope-risk: moderate
Directive: Keep stale-token policy dashboard-specific; the shared JSON-RPC client should expose close events without learning dashboard auth semantics.
Tested: npm --workspace web test (21 files, 106 tests); focused stale-token tests (5 files, 14 tests); npm --workspace web run typecheck; npm --workspace @hermes/shared run lint; npm --workspace @hermes/shared run typecheck; focused web eslint; git diff --check.
Not-tested: Manual browser smoke test across a real dashboard restart.
2026-08-04 15:26:37 +05:30
ehz0ah e43bc0b7aa fix(openviking): integrate reliability and configuration hardening 2026-08-03 20:35:47 +05:30
kshitij c0c45ab68a fix(plugins): keep loading gate when cached manifests include a /chat override
The sessionStorage seed set loading=false whenever any cache existed, which
defeats App.tsx's load-bearing pluginsLoading gate: with a cached manifest
that declares tab.override === "/chat", the persistent ChatPage host must
NOT mount before plugins resolve, or it spawns a PTY and gets yanked when
the override plugin takes over the route.

Seed loading=false from the cache only when no cached manifest overrides
/chat (canSeedLoadedFromCache); manifests are still seeded either way so
plugin routes register synchronously on refresh. Adds focused tests for
the gate, including the /chat-override case.
2026-08-03 18:48:04 +05:30
Allard 41bba3c13e test(plugins): export cache helpers and add focused fallback/refresh tests 2026-08-03 18:48:04 +05:30
Allard 057939c2af fix(plugins): validate cached manifests are an array 2026-08-03 18:48:04 +05:30
Allard 050b9533e0 perf(plugins): seed plugin routes from sessionStorage cache for instant render
- Plugin manifests are now cached in sessionStorage on fetch.
- On refresh, plugin routes are registered synchronously from cache, preventing unwanted redirects to /sessions.
- Removes the !pluginsLoading guard from the catch-all route in App.tsx, as plugin routes are now always available on first render.
- Background fetch always updates the cache and routes, so new/removed plugins are reflected after reload.
- Resolves the race condition where plugin pages would redirect to /sessions on hard refresh.
2026-08-03 18:48:04 +05:30
ethernet 6ecd335aa8 Merge pull request #75037 from NousResearch/sec-fixes
fix(sec): patch vulnerable deps + add publication-age floors and npm script allow-list

Co-authored-by: Kingsley Wong <7207924+datanerdie@users.noreply.github.com>
Co-authored-by: viky <vikyw89@gmail.com>
Co-authored-by: FT_IOxCS <237263164+ft-ioxcs@users.noreply.github.com>
Co-authored-by: 方明元 <fmy3@qq.com>
Co-authored-by: Yorkstone Supplies <58149681+sycamoregroupltd@users.noreply.github.com>
Co-authored-by: Steven Cuz Leath <Steven.Leath@gmail.com>
Co-authored-by: Kyle French <248366920+Dadmin88@users.noreply.github.com>
Co-authored-by: Eugeniusz Gilewski <egilewski@egilewski.com>
Co-authored-by: Christopher Gara <79837758+christopherrobin88@users.noreply.github.com>
Co-authored-by: LironTTG <147833337+LironTTG@users.noreply.github.com>
Co-authored-by: Austin Porada <bbasketballer75@gmail.com>
Co-authored-by: cresslank <9219265+cresslank@users.noreply.github.com>
Co-authored-by: Ion Mudreac <mudreac@gmail.com>
Co-authored-by: martinramos002 <262243228+martinramos002-bot@users.noreply.github.com>
Co-authored-by: Sensie-Agents <agents@joinsensie.com>
Co-authored-by: alexwill87 <173086651+alexwill87@users.noreply.github.com>
Co-authored-by: BullishMomentum56 <218643122+BullishMomentum56@users.noreply.github.com>
Co-authored-by: pintadoai <240097310+pintadoai@users.noreply.github.com>
Co-authored-by: Alfred Sahlberg <dinmail@gmail.com>
Co-authored-by: liuhao1024 <sunsky.lau@gmail.com>
Co-authored-by: Richard Ham <richard.ham@live.com>
Co-authored-by: jrcrittenden <jrcrittenden@gmail.com>
Co-authored-by: 峯岸 亮 <1920071390@campus.ouj.ac.jp>
Co-authored-by: Marcus Martini <6473852+napoleonmm83@users.noreply.github.com>
2026-07-31 14:07:32 -04:00
ethernet 390a1771b2 fix(sec): update npm deps to resolve npm audit warnings
pin brace-expansion to 5.0.8
update concurrently to 10.0.4
update electron-builder to 26.15.3
update eslint to 10.8.0
update eslint-plugin-perfectionist to 5.10.0
update @assistant-ui/react to 0.15.0
update @assistant-ui/react-streamdown to 0.3.8
update radix-ui to 1.6.7
update react-router-dom to react-router@8.3.0 - react-router-dom is no longer a standalone package, it just reexports react-router
remove @radix-ui/react-slot: we import this from `radix-ui`
remove eslint-plugin-react: we imported it, but never actually used it!
2026-07-31 13:42:03 -04:00
Austin Pickett 539e9b5c1b fix(dashboard): gate resume hydration on sanitized PTY payload
The resume wait notice cleared on the first nonempty raw PTY frame, but
the terminal is written sanitizer.next(text). The sanitizer collapses an
erase-only, all-newline, or partial-CSI resume frame to "", so a control-
only first frame hid the notice while xterm was still blank.

Gate hydration completion on the rendered payload actually written to the
terminal, and cover the control-only-first-frame case with a regression
test over the real sanitizer.

Co-authored-by: teknium1 <teknium@nousresearch.com>
2026-07-31 09:47:06 -04:00
xxxigm bb189bf6fe feat(dashboard): show wait notice while resumed chat history loads
Cover the blank TUI + blinking-cursor window on session resume, then hide
the notice as soon as the first real PTY payload arrives so history can
stream in visibly.
2026-07-31 09:47:06 -04:00
xxxigm 490f7048dd feat(dashboard): add resume loading overlay helpers
Extract overlay visibility helpers so the chat resume wait notice can be
tested without mounting ChatPage.
2026-07-31 09:47:06 -04:00
Brooklyn Nicholson f174c0b6bb fix(pairing): scope the approve/revoke endpoints to a profile
The gateway keeps one PairingStore per served profile, but every
`/api/pairing` endpoint built the global one. An operator managing a named
profile saw the wrong pending list, and approving wrote a grant into a
whitelist their running gateway never consults — the user stays locked out
while the UI shows them as approved.

`_pairing_store(profile)` now resolves per profile and validates the name
(400/404 on an unknown one). No `_profile_scope` needed: PairingStore
resolves the profile's home itself, so nothing process-global is swapped
across an await.

Both GUIs had to change to match. The listing rides the query param — for
the dashboard that meant deleting `pairing` from the "machine-global, must
NOT be rewritten" exclusion list, a comment this change makes false. The
mutating endpoints read the profile off the BODY, which no query-param
rewrite reaches, so approve/revoke send it explicitly on both surfaces.
2026-07-29 18:43:50 -05:00
Brooklyn Nicholson 37d0766ba5 fix(pairing): keep GUI approvals off the code brute-force lockout
Follow-up hardening on the request-id grant path.

approve_request took the same lockout treatment as approve_code: gated by
it, and recording a miss toward it. But the two paths defend different
things. The lockout exists to stop guessing at the 8-char code space over a
messaging channel; a request id is only ever obtained by an admin already
authenticated to the store, so a miss means the row they clicked went stale.
Counting those let a handful of clicks on a stale list lock the operator out
of `hermes pairing approve` for an hour — the GUI DoSing the CLI.

Also drops the `code`/`code_hash_prefix` compat fields from list_pending.
The hash prefix is what admin surfaces mistook for an approvable code in the
first place, and re-exporting the request id under the old `code` key just
preserves the ambiguity; both consumers in the tree read `request_id` now.
The 16-hex sniffing that had been copy-pasted into the CLI and the endpoint
(where a chained conditional consulted it against the wrong field) moves to
one owner, PairingStore.looks_like_request_id.

The endpoint no longer reports a 429 on the request-id path, where lockout
can't apply — a stale id surfaced as a bogus "locked out" while the platform
sat locked for something else entirely.
2026-07-29 17:51:40 -05:00
Flownium 774d92fbfa fix: approve listed pairing requests 2026-07-29 17:49:33 -05:00
Austin Pickett b8ceba97ed fix(web): make PTY resume sanitizer work against real PTY output
Review follow-up on the salvaged #47772 work. Three defects made the
filter a no-op or actively harmful in production, plus both Copilot
review items.

1. The blank-line burst filter never fired. pty_bridge.py spawns via
   ptyprocess.PtyProcess.spawn() and never calls setraw(), so the PTY
   line discipline runs with ONLCR: every LF the child writes reaches
   xterm as CRLF. The /\n{50,}/ pattern requires consecutive LF, so a
   real 1000-row burst matched nothing (verified against a live PTY:
   b"A"+b"\n"*5+b"B" is read back as b"A\r\n\r\n\r\n\r\n\r\nB").
   Now matches /(?:\r?\n){50,}/.

2. Bursts split across WebSocket frames were not collapsed. bridge.read()
   does os.read(fd, 65536) per drain tick and each read is forwarded as
   its own frame, so a burst spans frames and each fragment fell under
   the 50 threshold (3000 rows survived in a 40-byte-read simulation).
   The sanitizer now holds back a trailing newline run — including a lone
   trailing CR, since a frame can split a CRLF pair — and resolves it on
   the next frame or on flush.

3. Erase-code stripping was permanent, not resume-scoped. resumeParam is
   the durable session identity and is never cleared after connect, so
   every spinner/progress/status redraw in a resumed session lost its
   ESC[K and left stale glyphs. Suppression is now bounded to
   PTY_RESUME_SANITIZE_WINDOW_MS (30s) after connect; burst collapsing
   still applies for the life of the socket.

Copilot review items:
- flush() no longer writes a buffered partial CSI into xterm. #pending
  only ever holds an incomplete sequence, and emitting one leaves the
  parser in an in-escape state that swallows output after reconnect. A
  buffered newline run is still emitted (collapsed).
- Test expectations updated accordingly.

Tests: 29 cases (was 18), now using CRLF fixtures that match real PTY
output, plus cross-frame burst reassembly, CRLF-pair frame splits, and
post-window erase preservation. Full web suite 135 passing.
2026-07-29 12:21:48 -04:00
灵越羽毛 88e67508bc fix(web): replace stateless PTY regex with tested PtyResumeSanitizer stream helper
Addresses sweeper review feedback:

- Stateful buffer guards against CSI sequences split across WebSocket frames
- Newline threshold raised to \n{50,} — only targets Ink's pathological bursts
- Streaming TextDecoder with {stream: true} handles split UTF-8 bytes
- onclose flush drains any buffered partial escape
- Extract into web/src/lib/pty-resume-sanitizer.ts for independent testing

Added 17 vitest cases covering:
- applyPtyFilters: pass-through, burst collapse, short newlines, erase-line,
  erase-char, SGR preservation, empty string, mixed content
- PtyResumeSanitizer: complete chunks, 2/3-frame CSI splits, bare \x1b,
  \x1b[digit prefix, empty-chunk buffer integrity, instance isolation,
  onclose flush
2026-07-29 12:21:48 -04:00
灵越羽毛 5e37c94906 fix(web): gate ANSI filtering behind resumeParam and reuse TextDecoder
Addresses Copilot review:
- Reuse a single TextDecoder instance instead of allocating per message
- Only filter erase codes during session resume (resumeParam != null)
- Extract filter chain into named helper sanitizeResumeOutput()

Refs #47313
2026-07-29 12:21:48 -04:00
灵越羽毛 4b6e1e440e fix(web): strip ANSI erase codes and blank-line bursts from PTY stream
Ink two-pass virtual scrolling during session resume floods the
PTY output with \x1b[K (erase-line), \x1b[NX (erase-char), and
thousand-line \n bursts. In the Dashboard INLINE mode, xterm
main scrollback buffer absorbs these as blank rows.

Filter all three in ws.onmessage before they reach xterm.

Refs #47313.
2026-07-29 12:21:48 -04:00
Teknium 3ab583044a salvage(#51128): translate new session-filter strings across 17 locales; expose public get_session_rich_row for search hydration 2026-07-28 22:41:56 -07:00
Shannon Sands cb00495551 Add dashboard session filtering 2026-07-28 22:41:56 -07:00
HexLab98 c63e0cd3e3 fix(models): match bare Kimi Coding k3 when searching kimi
Kimi Coding discovers the flagship as wire id `k3`. Picker search used
only that id, so typing "kimi" hid it next to every other kimi-* model.
Add picker-only search aliases without changing the wire id.
2026-07-26 21:22:40 -07:00
teknium1 759f68bc25 fix(dashboard): stop rendering 'unknown' model placeholders in session lists
Sessions that die before title generation (or predate model tracking)
rendered as 'Untitled · unknown · 0 msgs' — two placeholders stacked in
one row reads as breakage to Hermes Cloud users. Now:

- Session rows omit the model segment entirely when the store has no
  model (no more 'unknown' + dangling separator).
- The Overview 'Recent Sessions' card falls back to the message preview
  as the row label (italic, same treatment as the History list) instead
  of a bare 'Untitled', and skips the duplicate preview paragraph when
  the preview IS the label.
2026-07-26 16:30:28 -07:00
erick713006 2df57fe641 perf(web): lazy-load dashboard routes and split heavy vendors
The production dashboard build packed almost every page plus xterm/three/
plot into one large JS chunk, which trips Vite's 500kB warning and slows
first paint even when the user only opens Sessions/Config.

- Lazy-load route pages in App.tsx behind Suspense
- Defer mounting the persistent embedded chat host (and xterm) until the
  first /chat visit, while keeping the sticky PTY latch afterward
- Add rolldown vendor codeSplitting groups (react, xterm, three, plot,
  motion, ui) and raise chunkSizeWarningLimit modestly to 600kB

Addresses #25912 (partial: route lazy-load + vendor splits + fallbacks;
not yet CI bundle analysis or documented entry budget).

Verified locally: npm run typecheck, npm run test (97), npm run build
with separate page/vendor chunks.
2026-07-26 16:30:28 -07:00
teknium1 6d1e08b2bc fix(dashboard): QA pass — log colors, nameless channels, config bool, UX gaps
Companion fixes from a full dashboard QA pass (every page dogfooded
live), on top of the cherry-picked #31863 header-slot fix:

- ChatPage: harden the header-slot effect further — useLayoutEffect and
  never write the slot while inactive, so the handoff commentary and
  ownership rule live next to the code.
- LogsPage: level classification used raw substring matching, so INFO
  lines carrying 'parse_errors=0' (or paths like errors.log) rendered
  red. New unit-tested classifier (web/src/lib/log-classify.ts) anchors
  on the hermes_logging level token with a word-boundary fallback.
- Channels API: plugin platforms (irc, ntfy, photon, teams, …) rendered
  as nameless title-cased cards ('Irc', 'Ntfy') with empty descriptions.
  Two root causes: (1) plugin discovery never ran in the dashboard
  server process, so plugin_entries() was empty; (2) Platform enum
  pseudo-members claimed plugin ids before the registry could attach
  labels. The catalog now discovers plugins explicitly and resolves
  plugin metadata first; added descriptions + docs links for bundled
  plugin platforms and the msgraph_webhook / whatsapp_cloud / relay
  enum members. Regression test sabotage-verified against the old
  enum-first ordering.
- Config schema: updates.refresh_cua_driver declared type 'bool'
  (schema vocabulary is 'boolean'), so the switch rendered as a text
  input holding 'true'.
- Page titles: '/mcp' rendered as 'Mcp' via the naive capitalize
  fallback; literal-label table now covers MCP/Files/Channels/Webhooks/
  Pairing/System (unit-tested).
- AuthWidget: skip the guaranteed-401 /api/auth/me probe in loopback
  mode — every dashboard load logged a console error for nothing.
- Model picker: with no filter, providers that actually have models
  float above the wall of '0 models' rows.
- Cron: empty state now carries an actionable Create button.
2026-07-26 15:17:30 -07:00
02356abc 8c3c52b008 fix(dashboard): stop ChatPage from clearing all pages' header action buttons
When embedded chat is enabled, ChatPage renders persistently outside
<Routes> but is initially hidden during the plugin-loading window
(~2-4s). Once plugins finish loading, ChatPage mounts for the first time.

Its header-slot effect had early-return branches for !isActive and !narrow
that actively called setEnd(null). Because the user was on /cron, /models,
/sessions, or any non-chat page, this wiped the action buttons that the
current page had already placed in the header.

Affected pages include:
- Cron page — CREATE button disappears
- Models page — 7D/30D/90D filter buttons disappear
- Sessions page — search box disappears

The fix collapses the two early-return branches into one and removes the
setEnd(null) calls. Now ChatPage only sets end when it actually owns the
slot (isActive && narrow), and lets the normal cleanup handle unmounting.

PageHeaderProvider already clears all slots on pathname change via
useLayoutEffect, so ChatPage's active clearing was redundant and harmful.

Fixes #31862

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-26 15:17:30 -07:00
Brooklyn Nicholson e39e3fb011 feat(web): add Arabic (ar) locale with RTL support
Adds the Arabic catalog to the dashboard, registers it in the locale list
and picker, and flips the document direction to RTL when Arabic is active.
Introduces a `defineLocale` merge helper (mirroring the desktop app) so the
Arabic catalog can be a partial override that falls back to English for any
untranslated key instead of hand-porting every future string.

Co-authored-by: morolab <ahmedmoro@gmail.com>
2026-07-24 12:10:00 -05:00
Teknium 23476207bc feat(moa): default advisor fanout to user_turn — the cheapest cadence
Flips the default fan-out cadence from per_iteration (advisors re-run on
every tool iteration, multiplying advisor spend by tool-loop depth) to
user_turn (advisors run once on the first message of each user turn; the
acting aggregator works the rest of the tool loop with that turn's
advice). Until per-mode benchmarks justify a costlier default, MoA
defaults to the cheapest, lowest-impact cadence (#67199).

One default for everyone — no split legacy/new-preset semantics; presets
that want per-step advising set fanout: per_iteration explicitly. All
three modes (user_turn / per_iteration / every_n:N) remain selectable;
every_n:1 still collapses to per_iteration (semantic identity), while
unparseable values now fall to user_turn (the default).

Docs updated with a default-change note; the per-iteration rerun test
pins its mode explicitly.

Co-authored-by: skyer-flyyy <188930297+skyer-flyyy@users.noreply.github.com>
2026-07-23 21:07:18 -07:00
Teknium d3fc27bbf8 fix(moa): make reference_timeout default inherit auxiliary config; filter recursion-guard skips
Follow-ups for salvaged #53784:

- reference_timeout now defaults to None = no per-preset override, so the
  reference fan-out inherits auxiliary.moa_reference.timeout (900s default)
  via call_llm's own per-task timeout resolution. The PR's 30.0s default
  would have cut off long-thinking advisors mid-response, and its 300s max
  cap capped legitimate explicit values — both removed. Explicit per-preset
  values are still honored as-is.
- _is_failed_reference also treats '[skipped: …]' recursion-guard notes as
  internal sentinels, keeping them out of both aggregator prompts.
- Dashboard/desktop TS types updated to number | null; web_server validator
  accepts null/empty as 'inherit'.
2026-07-23 18:40:09 -07:00
robbyczgw-cla ccdf171bcd fix(moa): contain failed reference details 2026-07-23 18:40:09 -07:00
oppenheimor ca294d3e62 feat(moa): add reference model toggles 2026-07-23 18:11:57 -07:00
Ben Barclay 7857d8737c feat(dashboard-auth): RFC 8252 native desktop sign-in (system browser + PKCE, no webview/cookies)
The Desktop app can now sign in to a gated gateway using the user's SYSTEM
browser and OAuth 2.0 for Native Apps (RFC 8252) instead of an embedded
Electron BrowserWindow, and authenticates with bearer tokens it holds itself
instead of relying on HttpOnly browser session cookies.

Why brokered: the upstream IDP (Nous Portal) binds client_id to the gateway
instance and only permits redirect_uris on the gateway's own origin, so a
desktop loopback redirect can't be a direct Portal client. The gateway
therefore acts as the authorization server TO the desktop and an OAuth client
TO the Portal, reusing the existing PKCE start_login/complete_login provider
path unchanged.

Server (Ben's dashboard-auth lane):
- native_flow.py: in-memory broker — binds the desktop's PKCE challenge to a
  completed Session, mints a single-use, short-TTL, PKCE-verified gateway
  authorization code. Constant-time compare, single-use (consumed before the
  PKCE check so a wrong verifier can't be retried), capacity-bounded.
- routes.py: GET /auth/native/authorize (starts the brokered PKCE login,
  loopback-only redirect_uri, S256-only), POST /auth/native/token (loopback
  code + verifier -> tokens in the JSON body, never Set-Cookie), POST
  /auth/native/refresh (desktop-held RT rotation). /auth/callback branches to
  mint a loopback code + 302 to 127.0.0.1 when a broker_state rides the PKCE
  cookie; the cookie/SPA path is untouched.
- middleware.py: the gate accepts Authorization: Bearer <access_token>,
  verified via the same verify_session provider stack (no cookie set/read),
  with the same "provider unreachable -> 503, not logout" semantics.
- web_server.py /api/status: advertise auth_flows (["cookie","native_pkce"])
  so clients can detect the capability; native_pkce only when a brokerable
  OAuth provider is registered.

Desktop (Ben's lane):
- native-oauth.ts: pure PKCE/capability/URL/callback/token helpers.
- native-oauth-login.ts: loopback-listener orchestration (system browser via
  openExternal, ephemeral 127.0.0.1 listener, state/PKCE verification), all
  I/O injected for testability.
- main.ts: capability-gated oauth-login IPC — native flow when advertised,
  automatic fallback to the existing embedded-webview cookie flow otherwise;
  tokens stored encrypted (safeStorage/OS keychain), REST + ws-ticket
  authenticated by bearer, transparent refresh, logout clears both shapes.

Tests: 18 server pytest (broker unit + full authorize->callback->token E2E +
cookieless bearer auth of a gated route + ws-ticket mint + capability
advertisement + refresh); desktop node --test/vitest for both pure modules
(PKCE, capability detection, callback CSRF, loopback round trip, timeout,
browser-open failure). Electron project typechecks clean.

Docs: website/docs/guides/desktop-native-signin.md.
2026-07-22 06:50:50 -07:00
HexLab 1cf2c763ef fix(dashboard): opaque MoA presets modal (stop page bleed-through) (#67410)
* fix(dashboard): make MoA presets modal opaque and readable

Card defaults to bg-background-base/80 glass, so the Mixture of Agents
dialog let the Models page bleed through — especially on Cyberpunk/mobile.
Portal an opaque dialog shell above the z-2 dashboard column, and ignore
Escape while the nested model picker is open.

* test(web): lock dashboard modal shell to opaque panel classes

Guard the MoA/dialog shell contract so glass Card defaults cannot
quietly return to modal panels, and Escape stays picker-aware.
2026-07-19 19:09:12 -04:00
Wesley Simplicio 07ba9e9266 fix(dashboard): don't let a provider-name query hide the selected provider's models (#65374) (#65413)
Co-authored-by: Simplicio, Wesley (ext) <wesley.simplicio.ext@siemens-energy.com>
2026-07-19 19:05:44 -04:00
xxxigm 3bcd0c1b00 fix(dashboard): only open the chat PTY once the chat tab is active (#59551)
* fix(dashboard): only open the chat PTY once the chat tab is active

The dashboard mounts ChatPage persistently (hidden with CSS) on every route
so the embedded chat PTY survives tab switches. But the PTY-connect effect
never checked whether the chat tab was active, so it opened `/api/pty` on
mount for ANY dashboard page. On a source/RPi install that spawns the whole
TUI + agent bootstrap (`Installing TUI dependencies…` → `npm install`) merely
by loading /sessions, /system, etc. — work the user never asked for, and the
trigger behind "dashboard loses custom themes on /chat load".

Gate the connect effect on a sticky activation latch: the PTY is not spawned
until the chat tab has been active at least once, and stays connected across
later tab switches so the persistence UX is preserved.

* test(dashboard): cover chat PTY activation latch

Asserts the invariant behind the fix: activation is sticky. It stays false
while the chat tab has never been active (so the persistently-mounted,
hidden ChatPage never opens /api/pty), flips true when the tab activates,
and stays true after the user navigates away (PTY persistence).
2026-07-18 00:50:13 -04:00