Commit Graph

176 Commits

Author SHA1 Message Date
Teknium df95f378a6 feat(dashboard): "Migrate to a single multiplexed gateway" on the System page
GET /api/gateway/migrate/plan returns the CLI plan JSON; POST
/api/gateway/migrate spawns `hermes gateway migrate --multiplex --yes`
detached (action log gateway-migrate.log). The Gateway card shows the
button only for a multi-profile install that is not yet multiplexed, and
disables it while listing the blockers.
2026-09-12 01:49:28 -07:00
Teknium 0dcadf6f41 revert: remove Collective Wisdom V1 (#94266)
Reverts the in-tree org skill-marketplace: hermes_wisdom package, three
model tools, CLI/gateway/desktop/dashboard/Telegram/Slack surfaces.

Later non-Wisdom work on shared files (guest onboarding i18n, dashboard
startup schema, Slack adapter, tui_gateway) is kept; Wisdom-only call
sites and config were stripped from those files.
2026-09-11 11:54:49 -07:00
shannonsands a6ee31f55a feat(wisdom): add Hermes Collective Wisdom Agent V1 (#94266)
* feat(wisdom): add trusted publish and install foundation

* feat(wisdom): add private contribution loop

* feat(wisdom): add managed consumption workflows

* fix(wisdom): close cross-repository safety gaps

* fix(wisdom): align local package and lifecycle policy

* fix(wisdom): require explicit profile setup

* docs(wisdom): repin reconciled gateway head

* fix(wisdom): fence content downloads and approval receipts

* docs(wisdom): record generation-fenced downloads

* docs(wisdom): record unified delivery PR

* fix(ci): stop passing invalid classifier inputs

* docs(wisdom): remove internal requirements ledger

* feat(wisdom): localize dashboard and desktop copy

* feat(wisdom): complete local contribution and consumption UX

* style(wisdom): satisfy desktop lint

* chore(wisdom): refresh requirements pin

* test(dashboard): allow formatted profile copy

* test(wisdom): stabilize desktop interaction coverage

* fix(wisdom): surface dashboard action failures

* fix(wisdom): add repeatable Portal demo login

* feat(wisdom): add actionable skill notifications

* feat(wisdom): add notification install and update actions

* fix(wisdom): make Telegram skill alerts actionable

* fix(wisdom): always refresh demo Agent login

* feat(wisdom): embed Telegram notification actions

* fix(wisdom): preserve Telegram notifications after actions

* fix(wisdom): keep Telegram notification cards readable

* feat(wisdom): add Telegram candidate approval flow

* feat(wisdom): explain Telegram qualification reasons

* fix(wisdom): reconcile cross-surface candidate actions

* feat(telegram): add Collective Wisdom management command

* chore(wisdom): refresh Gateway contract pin

* chore(wisdom): advance Gateway contract pin

* feat(wisdom): align command UX across clients

* feat(slack): add Collective Wisdom management parity

* feat(wisdom): add security and professionalism reviews

* feat(wisdom): add first-time qualification guidance

* feat(wisdom): simplify qualification sharing choices

* feat(skills): add optional editorial metadata

* feat(wisdom): enrich legacy skill presentation

* fix(wisdom): harden review and update boundaries

* fix(wisdom): emit canonical review timestamps

* fix(wisdom): align with merged gateway and main

* wisdom: add agent-led sharing core (policy, evidence, schemas, templates, delivery, weekly job, share/install flows)

- hermes_wisdom/agent_led/: policy resolution (server > local > defaults),
  7-day evidence builder that excludes bundled/hub/managed skills and
  dismissed/handled/recently-suggested content hashes, strict pydantic
  schemas for agent output with repair-or-reject, fixed copy templates
  (Share / Teammate / Published / Update / Mute), idempotent retried
  delivery ledger with stale-action resolution, weekly review job,
  resumable Share and Install flows.
- prompts/: candidate review, recipient recommendation, share packaging.
- tests/wisdom/test_agent_led.py: 30 tests.

* wisdom: agent-led renderers and button action dispatcher

- render.py: Telegram HTML, Slack blocks, Desktop payload; editorial name
  is the emphasized line, product label stays separate.
- actions.py: resolve opaque wa:<action>:<dedup> targets via the delivery
  ledger; Not now -> dismissal, Mute -> fixed options, Share -> resumable
  packaging flow, Install/Update -> plan command. Never publishes/installs.

* wisdom: CLI verbs, agent_led config default, conversational catalog skill

- hermes wisdom browse/review-week/act/share/dismiss/mute (all --json).
- wisdom.agent_led config block, default enabled.
- SKILL.md rewritten so natural-language catalog questions map to the CLI
  verbs, share/install flows and fixed notification templates.

* wisdom: wire agent-led weekly review into gateway tick and Telegram buttons

- gateway housekeeping tick calls maybe_run_weekly_review with a home
  channel sender when a Telegram adapter is available.
- Telegram: wa: callbacks resolved through the ledger (stale-safe), mute
  duration keyboard, send_wisdom_agent_recommendation rich card + fallback.

* fix(wisdom): integrate local mediation and harden model and setup boundaries

* fix(wisdom): honor authoritative recommendation policy and defer on failure

* fix(wisdom): synchronize opaque suppression and recheck delivery preferences

* feat(wisdom): route weekly selection through the session-owned assessment queue

* fix(wisdom): prepare and submit the reviewed generated share package

* feat(wisdom): separate native Share preparation from publication consent

* feat(wisdom): sync native mute choices through a leased preference outbox

* feat(wisdom): bind native mute controls to durable preference choices

* feat(wisdom): add scoped desktop and dashboard notification settings

* fix(wisdom): revalidate feed recommendations before assessment and delivery

* fix(wisdom): persist validated delivery receipts before completing notices

* feat(wisdom): add private notification claim and receipt client

* Persist Wisdom send reservations and recover delivery acknowledgements

* Route legacy Wisdom controls through current native review

* Add typed private Wisdom operation outcome client

* fix(wisdom): make agent-led advice usable in the local demo

* fix(wisdom): keep requested consent outside proactive limits

* fix(wisdom): distinguish unavailable assessments and preserve digest text

* fix(wisdom): assess ongoing usefulness beyond the current task

* fix(wisdom): restore immediate qualification sharing controls

* fix(wisdom): separate qualification review from installation advice

* fix(wisdom): collapse review checklists and simplify sharing copy

* fix(wisdom): show compact sharing progress and publication receipts

* fix(wisdom): require credential prefixes rather than matching skill names

* fix(wisdom): finish package checks before presenting sharing consent

* fix(wisdom): scan local skills before qualification cards

* fix(wisdom): update moderation results on existing sharing cards

* fix(wisdom): keep sharing review accessible from receipt cards

* fix(wisdom): align mediated review cards and collapsible checks

* fix(wisdom): clarify clean security summary wording

* fix(wisdom): normalize consent plans and add explicit recheck

* fix(wisdom): keep install and update receipts concise

* fix(wisdom): collapse assessments and deduplicate operation cards

* fix(wisdom): restore private Portal review from native cards

* fix(wisdom): sync Portal publication to original consent card

* fix(wisdom): show local skill version on sharing cards

* fix(wisdom): skip agent recommendations for self-published versions

* fix(wisdom): simplify candidate notices and local-edit recovery copy

* feat(wisdom): submit locally reviewed packages with one confirmation

* feat(wisdom): expose safe receipt and outcome sync recovery

* wisdom: onboarding notice says detect and share, names the user's own skill

Copy review from the product owner on the first and returning
qualification notices (fixed delivery mode):
- the feature blurb now says the org enabled detection *and sharing*
- both notices say the detected skill is one the user created
- both close with an exclamation mark

Applied identically to hermes_wisdom.notice, the desktop and web i18n
strings, and the tests that assert the sentences.

* wisdom: one opener, no approval line, ask to share after the skill is shown

Product owner review of the candidate card.

- The Hermes written card now opens with the same sentence as the fixed card
  ("Your organisation has enabled Collective Wisdom, a feature designed to
  automatically detect and share useful skills across all team members.")
  instead of its own blurb, so there is one first time message.
- "Nothing is shared without your approval." removed from Telegram, Slack
  and Desktop. The buttons already make the permission explicit.
- "Would you like to share?" no longer appears before the skill is named.
  It is now the last line, after the skill name, description, why suggested
  and the checks, and reads "Would you like to share it?" (matching the
  agent led template wording).

Tests updated for the new order; proposalNotice removed from all desktop locales.

* wisdom: American spelling, organization

Product owner decision: user facing copy uses American spelling.
Changes "Your organisation" to "Your organization" in the chat notice,
the Hermes written card opener, the desktop and web strings, and the
tests that assert them. Identifiers such as nas_organisation:* and the
German and French locales are untouched.

* wisdom: candidate card copy round 4 (owner review)

Apply the product owner's round 4 copy decisions to the Hermes Collective
Wisdom candidate card on Telegram, Slack, Desktop and the shared views:

1. Hermes-written cards are titled "Hermes Collective Wisdom" instead of
   the bare "Collective Wisdom".
2. The "Reusable skill ready to review" line is gone from the candidate
   card (Telegram rich card and plain fallback, legacy agent-led share
   template).
3. The skill name and description are labelled: "Skill name: <name>" and
   "What it does: <description>" (Telegram, Slack, Desktop).
4. "Why suggested:" is now "Why others might benefit:".
5. A passing professionalism review reads "Safe to share at work ✓ (no
   inappropriate content found)" with no per-check bullets and no "Pass";
   a failed review reads "Needs a look before sharing at work (possible
   inappropriate content)" and lists only the checks that flagged
   something. Pending/unavailable wording is unchanged.
6. Telegram button toasts: "Will ask later...", "Preparing more
   details...", "Sharing...".
7. Qualification reasons: "You used this skill consistently across many
   days." and "You've really refined this skill."
8. prompts/wisdom_candidate_review.md asks for a compelling
   editorial_name, a simple one_line_description and a compelling
   why_coworkers_benefit under 300 characters; "Be concise and
   convincing." becomes "Be concise and compelling: the goal is that the
   user wants to share it."

Tests updated for the new strings; review_text() gains direct coverage.

* wisdom: re-apply owner copy after rebase

- Native share cards (advice_view/interaction_view): drop the approval line, ask "Would you like to share it?" as the last line after the checks
- Hermes-written completion card titled "Hermes Collective Wisdom"
- Qualification reasons use the owner wording (consistently across many days / really refined)
- American spelling (organization) in remaining English copy
- Desktop test asserts the current Share button; web test matches the returning notice

* fix(wisdom): pin reconciled Gateway and verify Unicode hash vectors

Pin Gateway 60cd2d6b613ae3cd4a6e65155d1142006d907e78 and byte-identical producer artifacts. Verify every content-order case and package-manifest binding. Validation: 186 focused Python tests, Ruff and contract verifier.

* fix(wisdom): reconcile optional SDK tests and frontend lint

* fix(wisdom): default to agent-written notification summaries

* fix(wisdom): restore deferred install review and browse controls

* feat(wisdom): inspect installed setup with exact package provenance

* feat(wisdom): run native-approved installed setup steps with durable evidence

* fix(wisdom): recover interrupted setup with explicit native consent

* feat(wisdom): hand native installs into guided setup review

* fix(wisdom): continue requested setup with fixed notification copy

* fix(wisdom): preserve setup while waiting for a session model

* fix(wisdom): expose canonical setup review controls on desktop

* fix(wisdom): resume setup after recorded automatic updates

* fix(wisdom): make missing setup prerequisites recheckable

* chore(wisdom): align Agent with verified Gateway contract

* fix(wisdom): stop guessing team slugs in portal links

* fix(wisdom): retire pending advice on account sign-out

* fix(wisdom): cancel advice after terminal account revocation

* fix(wisdom): fence feed responses across account sign-out

* fix(wisdom): checkpoint signed-out feed before reactivation

* fix(wisdom): link proactive advice to scoped notification settings

* fix(wisdom): coalesce queued publication recommendations by version

* fix(wisdom): keep package review navigation local and deferable

* fix(wisdom): reflect installed state in discovery controls

* fix(wisdom): show exact checks before command confirmation

* chore(wisdom): pin bounded analytics privacy contract

* chore(wisdom): pin retired legacy notification contract

* feat(wisdom): review publisher usage with exact sharing copy

* fix(wisdom): align discovery and review check summaries

* fix(wisdom): show expired consent before confirmation

* fix(wisdom): require fresh review for legacy install controls

* fix(wisdom): preserve review expiry across check toggles

* fix(wisdom): retain update policy in native install reviews

* fix(wisdom): surface failed native card edits

* fix(wisdom): persist local command approval reviews

* fix(wisdom): use saved approvals for messaging commands

* test(wisdom): provide scan result in setup handoff fixture

* test(wisdom): exercise Telegram approvals with saved review state

* fix(wisdom): retain suppression policy for offline deferral

* fix(wisdom): reconsider candidates after deferred suppression expires

* fix(wisdom): bind review checks and report verified readiness separately

* fix(wisdom): persist accepted publication intent and recover exact outcomes

* fix(sync): pin UTF-8 tree ordering across writers

* chore(wisdom): pin organisation-scoped Gateway authorization

* fix(wisdom): restrict consent delivery to user-facing sessions

* chore(wisdom): refresh reviewed Gateway contract pin

* fix(wisdom): preserve kept tools in Blank Slate exclusions

* test(auth): reset anonymous fixture with a profile-scoped cache

* fix(wisdom): gate local surfaces and work on current profile entitlement

* fix(wisdom): invalidate quiet tool cache on entitlement changes

* test(wisdom): authorize local consent gateway fixtures

* fix(wisdom): keep entitlement decoding free of native crypto imports

* test(wisdom): provide local entitlement to demo CLI subprocess

* ci: leave upstream workflow unchanged in Wisdom PR

* fix(wisdom): ship package and contracts in Nix wheels

---------

Co-authored-by: hbizi <36184542+hbizi@users.noreply.github.com>
2026-09-11 19:04:06 +10:00
teknium1 87cc4de430 fix(dashboard): place the deactivation ref reset after the callbacks that also write those refs
PR #106445's effect was inserted above `reconnectPty` / `startFreshPty` /
`startFreshDashboardChat`. React Compiler's `react-hooks/immutability`
rule then treats `ptyInputLineRef` and `mobileReplacementInputUntilRef`
as effect-owned values and flags the six pre-existing assignments in
those callbacks as errors ("This value cannot be modified"), which is
what turned the `JS & TS checks` job red. Declaring the effect after the
callbacks keeps the rule quiet; eslint on the file is back to main's
0 errors / 3 warnings.

Adds one pure-function vitest on `normalizePtyMobileInput` proving the
symptom: a stale tracked line ("hel") makes an in-window re-emission of
"hello" come out as 3×DEL + "hello", which over a composer already
holding "hello" leaves "hehello"; with the tracker reset the same input
passes through untouched.
2026-09-09 10:48:49 -07:00
Teknium d47adec28f Merge origin/main into feat/plugin-catalog
Python plugin CLI/loader/web/tui files taken from main wholesale; the
catalog layer is re-ported onto main's decomposed shapes in the
following commits. plugin_index.py removed (catalog is the sole
discovery system).
2026-09-09 04:15:27 -07:00
Teknium fd3565deec fix: remove dedicated user-facing output cap controls 2026-09-07 06:15:43 -07:00
Austin Pickett 04fd0172cd fix(dashboard): stop the embedded TUI repainting on every OS app-switch (#103165)
* feat(pty_bridge): mark the dashboard-spawned TUI with HERMES_PTY_HOST

Ink needs to know when its emulator is the dashboard's xterm.js rather
than a native terminal, so it can drop hidden-tab recovery work that only
makes sense for emulators that coalesce output.

Co-authored-by: Raymond <supere989@users.noreply.github.com>

* fix(tui): skip the focus-in erase+repaint under the dashboard PTY

Ink answers a DECSET 1004 focus-in with a full clear+repaint to heal rows
a native emulator may have dropped while the tab was hidden. xterm.js fed
by the dashboard WebSocket never drops frames, and it reports focus on
every OS window blur/focus, so under the dashboard that repaint was a
visible "session reloaded" flash on every alt-tab. Keep the mode
re-assert and keep delivering the focus report to TerminalFocusProvider
(the composer hides its cursor on blur); only the repaint is skipped.

Co-authored-by: Raymond <supere989@users.noreply.github.com>

* fix(web): restore terminal focus after an OS app-switch

Alt-tabbing away and back lands browser focus on <body>, so Ctrl+V never
reached the composer. Pull focus back into xterm on window focus under
the same ownership rule tab activation already uses, extracted into
shouldRestoreTerminalFocus so both paths share it.

Co-authored-by: Raymond <supere989@users.noreply.github.com>

---------

Co-authored-by: Raymond <supere989@users.noreply.github.com>
2026-09-04 20:24:02 -04:00
liuhao1024 74b00d7a97 fix(dashboard): route every per-row session request at the row's owning profile (salvage #99387)
The Sessions page listed rows stamped with their owning profile but sent
delete/bulk-delete to the global management profile, which stays "" while
the sticky active profile equals the dashboard process's own — so the
request opened the process store, missed, and returned a false
`already_absent` success while the row survived in profiles/<p>/state.db.

Same class at three sibling sites the PR didn't touch: renameSession,
exportSessionUrl and the expanded-row getSessionMessages read. One
`rowProfile(id)` owner now feeds all four (+ bulk delete); unstamped rows
(search results) fall back to the management profile as before.

Test trimmed to one jsdom scenario driving all four row actions.

Co-authored-by: Teknium <teknium@nousresearch.com>
2026-09-02 06:47:55 -07:00
Teknium df4b3733ba fix(cron): every last_status consumer renders delivery_failed explicitly (dashboard badge, Desktop inspector, /cron list, docs)
Audit of every last_status reader outside the scheduler (rg last_status across
web/, apps/desktop/, hermes_cli/, tui_gateway/, tools/, scripts/, website/):

- web dashboard CronPage: last_status was never rendered at all — a
  delivery_failed job showed a green 'scheduled' badge and only a small red
  'delivery: ...' line. New pure cronLastResult() helper maps the closed
  literal set to tones (ok=success, delivery_failed/blocked_config=warning,
  error/unknown=destructive) and the card now shows an amber
  'delivery_failed' badge (title = last_delivery_error).
- Desktop hermes-bots routine inspector: 'Last result' printed the raw
  literal; routineLastResult() spells out each one ('Ran, but delivery
  failed', 'Blocked by configuration (not run)', ...), unknown passes through.
- /cron list (cli_commands_mixin): 'Last run: <ts> (delivery_failed)' now
  appends the delivery reason, since last_error is None for those runs.
- hermes cron list/doctor and the cronjob tool already handled the literal
  on this branch; no consumer compared == 'ok' for success apart from the
  cronjob manual-run path, which the branch already fixed.
- developer-guide/cron-internals.md: table of last_status literals + which
  detail field carries the reason.

Live repro (real 'hermes dashboard' on a temp HERMES_HOME with a
delivery_failed job, CronPage rendered against the live /api/cron/jobs):
before — badges [scheduled, default, telegram:123]; after — badges
[scheduled, delivery_failed (warning tone, title 'telegram: 502 Bad
Gateway'), default, telegram:123].
2026-09-02 00:52:58 -07:00
Ryan Currah c8a7c6c36b fix(dashboard): remove deleted custom env keys 2026-08-31 03:37:43 -07:00
joaomarcos 0099f250c2 fix(auth): close Anthropic OAuth review gaps 2026-08-29 18:34:35 -07:00
Teknium 7e17ff0ab3 Merge origin/main into feat/plugin-catalog — reconcile with landed index/manifest-v2 tracks 2026-08-27 21:29:42 -07:00
chelsealong 9c013eaaf8 fix(dashboard): follow scroll on implicit active-session resume (#93518)
pty_ws already fell back to the per-channel active-session file when a
/chat WS connects with no ?resume= param, replaying the whole session
into the PTY, but the frontend only pinned xterm's viewport to the
bottom when resumeParam came from the URL (#59591). The implicit path
had no way to learn a replay was happening, so the viewport stayed at
the top of the scrollback.

pty_ws now sends a one-off JSON control frame naming the session id it
resolved from the active-session file, before any PTY bytes; PTY
output itself always arrives as binary frames, so this is unambiguous
on the wire. ChatPage tracks an `effectiveResume` value seeded from
resumeParam and updated when this control frame arrives, and the
existing follow-scroll/sanitizer/hydration logic keys off it instead
of the URL param alone.

Fixes #93518.
2026-08-24 03:21:49 -07:00
Teknium 1bf8bd2c7d feat(models): 'ox alpha' now finds x-preview-f-free in every model picker
The OpenCode Zen wire slug for the Ox Alpha stealth model is opaque
(x-preview-f-free); users searching the picker for 'ox' or 'ox-alpha'
found nothing. Adds the search alias across all four synced alias
tables (CLI, desktop, web, TUI) plus tests. Wire id is unchanged and
still what renders and gets sent to the provider, matching the k3 →
kimi-k3 precedent. No canonical-dedup collision with opencode-go's
keyed ox-alpha-free slug.
2026-08-21 14:38:19 -07:00
Teknium a1682376ca feat(profiles): rename any agent — the default profile gets a display name (#45624)
`hermes profile rename default <name>` (and the Desktop/dashboard rename
flows) now set a presentation-only `display_name` in profile.yaml instead
of erroring. The canonical id stays "default"; resolution, comparison,
and spawn paths are untouched. Named profiles keep real renames and their
display_name survives the move.

Surfaces: profile list/show/status, /profile (text only — data.profile
stays canonical), dashboard ProfilesPage, TUI-gateway profiles.list, and
Desktop (rail, switcher, Manage page, and the Bot Mode roster via a
displayName fallback so a renamed default shows its name, not "default").

Slimmer redo of the direction in PR #87760 by @yxssxn — thanks; see PR
body for what changed vs that approach.
2026-08-18 02:27:18 -07:00
Teknium cb1b1da219 fix: surface missed cron fires as last_fire_error on the job record
On hosted deployments a scheduled fire that cannot be forwarded to the
gateway api_server (dead 8642 listener, gateway down) was invisible
outside gui.log: no execution row is created because the claim never
happens, so `cronjob list` showed a healthy job that silently missed
days of scheduled runs (4 consecutive nightly misses in the field,
diagnosed only by log grep).

Changes:
- cron/jobs.py: note_fire_forward_failure() durably stamps
  last_fire_error ({at, detail}) on the job record; mark_job_run clears
  it on the next successful run so it always describes current
  auto-fire health (mirrors preflight_alerted/drift_alerted).
- hermes_cli/web_routers/cron.py: the dashboard fire webhook stamps the
  job on the gateway-unreachable path, best-effort (never disturbs the
  503/Retry-After retry contract or the OOF-266 intentional-stop drop).
- tools/cronjob_tools.py: _format_job carries last_fire_error so the
  agent-facing cronjob list surfaces it.
- hermes_cli/cron.py: `hermes cron list` prints a red
  "Missed scheduled fire" line.
- web/: dashboard CronPage renders the miss; api.ts type updated.
- gateway/run.py: one-time startup warning when an external cron
  provider is active but the api_server adapter is not running (the
  fire path is dead-on-arrival; most common cause is API_SERVER_KEY
  missing from an unsupervised gateway relaunch).
- website/docs: cron doc section on missed fires.
2026-08-17 11:29:10 -07:00
Teknium 3ef63373f8 Port from paperclipai/paperclip#10875: route all dashboard copy actions through the HTTP-safe clipboard helper
Self-hosted dashboards served over plain HTTP on a LAN have no
navigator.clipboard (insecure context), so every direct writeText call
silently failed. web/src/lib/clipboard.ts already ships the HTTP-safe
copyTextToClipboard fallback but only OAuthLoginModal used it; ChatPage
(OSC 52 + Ctrl/Cmd+Shift+C), ProfilesPage, SystemPage, and WebhooksPage
all bypassed it. Route them through the helper and add a source-level
regression test that rejects any new direct clipboard write outside
lib/clipboard.ts (clipboard reads are exempt: no legacy fallback exists).

Sabotage-verified: the guard test fails when a direct write is introduced.
2026-08-16 22:10:29 -07:00
Teknium ea29702749 feat(cron): --continuity / --no-continuity flags on hermes cron create/edit
CLI parity for the continuity toggle:

- subcommands/cron.py: --continuity on create; --continuity / --no-continuity
  tri-state pair on edit (same store_const pattern as --no-agent/--agent)
- cron.py: forwarded to the cronjob tool; created/edited job summaries print
  a "Continuity: on" line
- cronjob_tools._format_job: reports continuity as an explicit boolean and
  strips the reserved 'self' entry from the reported context_from list
- cron-job.ts: form reader accepts both shapes (raw store record with 'self'
  inside context_from, or formatted record with the explicit flag)
- docs: CLI flag examples in the continuity section

E2E (real argparse -> cron_create/cron_edit -> jobs.json in temp HERMES_HOME):
create --continuity stores ['self']; edit --no-continuity clears; edit
--continuity restores; default-off unchanged. 91 cron/tool tests + 16 CLI
cron tests + vitest 10/10 pass.
2026-08-16 22:09:28 -07:00
Teknium 0b13cafffa feat(cron): continuity toggle across dashboard, Bot Mode routines, and TUI cron RPC
Wire the continuity flag through every cron-creation surface, not just the
model tool:

- dashboard (web/): checkbox in the cron job editor; form state round-trips
  the stored reserved 'self' entry into the toggle and strips it from the
  context_from textarea; web_server dashboard validator skips 'self'
  (create precedes the job's existence)
- Bot Mode Routines tab (hermes-bots plugin): Continuity checkbox in the
  New Cronjob dialog, forwarded through cron.manage
- tui_gateway cron.manage RPC: optional continuity param on action=add

vitest cron-job suite 10/10 (4 new), tsc app project clean, py_compile clean.
2026-08-16 22:09:28 -07:00
Shannon Sands 67710548ec fix(web): keep the chat terminal input line above the mobile soft keyboard (NS-434)
On mobile the on-screen keyboard overlays the layout viewport instead of
resizing it (iOS Safari always; Android Chrome under its default
interactive-widget=resizes-visual). The dashboard shell is a fixed h-dvh
column, so the xterm host's bounding box never changed when the keyboard
opened: fit() computed identical (cols, rows), no RESIZE reached the PTY,
and the Ink input line — drawn at the bottom of the grid — stayed hidden
under the keyboard.

Fix, in three parts:

1. Keyboard-inset handling (new web/src/lib/keyboard-inset.ts).
   computeKeyboardInset() measures the layout-viewport region obscured by
   the keyboard via window.visualViewport
   (innerHeight - vv.height - vv.offsetTop, with an 80px floor so
   collapsing URL-bar chrome doesn't thrash the grid). ChatPage applies
   it as bottom padding on the terminal wrapper, which shrinks the host →
   the existing ResizeObserver/fit path recomputes rows and sends RESIZE →
   Ink redraws the input line above the keyboard. Listens on both vv
   resize and scroll (offsetTop changes arrive as scroll events on iOS).

2. interactive-widget=resizes-content in the viewport meta. Android
   Chrome 108+ then resizes the layout viewport natively and the JS inset
   computes ~0 (harmless no-op); iOS ignores the directive and takes the
   JS path.

3. Scroll pinning. iOS auto-scrolls the page to reveal xterm's hidden
   textarea on focus, which drags the fixed shell offscreen. While a
   keyboard inset is active we pin window/scrollingElement scroll back to
   0 and term.scrollToBottom() so the freshly-resized input line stays in
   view.

Unit tests cover the inset math (thresholds, offsetTop, rotation races,
fractional geometry, non-finite guards). Grid-level behavior needs a real
device pass — DevTools emulation doesn't model keyboard insets.
2026-08-16 22:06:16 -07:00
fangliquanflq 0b8a09759c fix(sessions): address prune skip review notes 2026-08-16 01:55:25 -07:00
fangliquanflq 29dfbf2d6a fix(sessions): surface open sessions skipped by prune 2026-08-16 01:55:25 -07:00
Teknium fbe4d73051 test: type the WebSocket mock to satisfy docker-build tsc strictness 2026-08-15 01:05:39 -07:00
null-runner 4e6d863d0e fix(dashboard): gate shortcut sends on PTY state
The word-delete branches sent ^W / ESC d whenever the socket was OPEN,
bypassing the shouldBlockPtyInput gate that term.onData applies to
every other keystroke, so a reconnecting/closed session could still
receive shortcut bytes. Route both branches through a shared guarded
sender that applies the identical socket + PTY state check, and cover
the non-open PTY states in the lib tests.
2026-08-15 01:05:39 -07:00
null-runner be250390db fix(tui): restore copy and cut shortcuts 2026-08-15 01:05:39 -07:00
Denis H 00516e6e8e test(dashboard): cover delayed IME fallback after unrelated input 2026-08-15 00:33:49 -07:00
Denis H 0d40955fd1 fix(dashboard): handle chunked IME composition input 2026-08-15 00:33:49 -07:00
Denis H 434f1e954c fix(dashboard): retain composition after unrelated input 2026-08-15 00:33:49 -07:00
Denis H a428531ef6 fix(dashboard): preserve consecutive IME composition input 2026-08-15 00:33:49 -07:00
Denis H 874ae74a10 fix(dashboard): keep mouse input out of IME fallback 2026-08-15 00:33:49 -07:00
Denis H 3d9fdb2e2b fix(dashboard): avoid duplicate IME fallback input 2026-08-15 00:33:49 -07:00
Denis H c066b5a1d3 test(dashboard): cover IME fallback lifecycle 2026-08-15 00:33:49 -07:00
Denis H 0924e33369 fix(dashboard): defer IME fallback until xterm input settles 2026-08-15 00:33:49 -07:00
Denis H 6ece2da738 fix(dashboard): forward committed dead-key input to PTY 2026-08-15 00:33:49 -07:00
Denis H 278c6ebcb7 test(dashboard): reproduce dropped dead-key composition input 2026-08-15 00:33:49 -07:00
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
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
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 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
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
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 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