Commit Graph

249 Commits

Author SHA1 Message Date
teknium1
dacb06ea93 feat(plugins): show on_session_finalize return messages to the user
A plugin's on_session_finalize hook can now return a str or {"message": str}
and Hermes shows it at the session boundary without starting a turn or
rewriting the model reply:

- CLI /new: printed before the new session; CLI quit: printed above the
  exit summary; one-shot -q/-z: printed to stderr (stdout stays the answer)
- TUI: session.close returns the messages; /new shows them as system lines
- Gateway /new and shutdown: sent to the owning chat via the adapter, while
  adapters are still connected

on_session_end stays observer-only: it fires after every turn, not at a
session boundary. Gateway finalize dispatch moves to
gateway/run_shutdown_session_end.py.
2026-10-08 04:11:24 -07:00
alt-glitch
2c8df45ee5 Merge remote-tracking branch 'origin/main' into sid/onboarding
# Conflicts:
#	agent/conversation_loop.py
#	tui_gateway/prompt_turn.py
2026-10-07 19:29:35 +05:30
Teknium
6cc1efd8f9 feat(voice): auxiliary.voice_chat routes spoken voice turns to their own model
Voice replies had to come from the session's main model, so a heavy model made every spoken
answer slow (#83894). A new auxiliary slot, voice_chat, picks the model for voice turns:

- CLI/TUI voice mode, the Desktop voice conversation (prompt.submit voice_turn) and messaging
  voice notes mark the turn; agent/voice_turn_route.py binds the route after the prompt, row and
  preflight compaction were settled against the main model and undoes it before memory sync and
  background review, so the next typed turn and the review run on the main model.
- A turn larger than the voice model's compression threshold stays on the main model with a
  one-time notice: switching to voice never compacts the conversation.
- Usage lands in session_model_usage under task voice_chat; it never rewrites the session's
  recorded model/route, so resume keeps the model the user picked.
- Slot appears in Desktop Settings -> Models, the dashboard, and `hermes model`; GPT-Live
  delegations keep using the session model.
2026-10-07 01:35:11 -07:00
alt-glitch
995bc1c0e7 feat(free-tier): offer sign-in after finished tasks, backing off
A free-tier user is offered sign-in 3 minutes after their first finished
task, whether or not they send another prompt. Later offers each need
another finished task after the previous one, come 3 minutes after that
task, and never sooner than 1h, 4h, then 24h after the previous offer, so
a user who keeps saying "Not now" hears it less often.

- hermes_cli/free_tier_offer.py: the policy. One install-wide record
  (<default root>/free_tier/sign_in_offer.json, file lock + atomic write);
  claim_offer() is compare-and-set so two windows never both show it.
  Nothing is due off the free tier.
- tui_gateway/free_tier_task_done.py: a "finished task" is a completed
  turn with a reply, on the free-tier agent, not a crash resume, and with
  no steer/redirect/interrupt/busy-typing while it ran. Called from
  prompt_turn after a complete turn, before message.complete goes out, so
  a status read prompted by that event already sees the record. Intervention is reported by the agent
  (result["user_intervened"], reset per turn) and marked on the session by
  the steer/redirect/interrupt RPCs and busy submits.
- Wire: free_tier.status gains nudge_due_in (seconds, 0 = now, absent when
  none is pending); new free_tier.claim_nudge -> {claimed}.
2026-10-07 12:26:42 +05:30
alt-glitch
cfc17affe8 refactor(onboarding): drop profiles.remember_onboarding
The old renderer handoff was the RPC's only caller; the setup flow hands
off through start_chat and writes no memory. Removes the writer module,
the handler and its contract models; the TypeScript contract is
regenerated.
2026-10-07 11:38:21 +05:30
alt-glitch
8b89b68fb4 Merge origin/main into sid/onboarding
Brings the onboarding branch up to main 0c2ff854a5. Where a Wave A PR
already landed a reviewed version of a piece of this branch (free tier
#134049, keyless web #134048, app resolver #134050, bundle and uninstall
#134051, catalog card and toolsets #134052, built-in tour #134054, stdin
guard #134055, retired key #126146), main's version wins. The PM long-path
files also take main's version: that work lands through #130430.

Kept from the branch on top of main:
- the setup and start_chat toolsets, declared with main's platforms key
  (desktop only), and the setup_choose server request contract;
- the setup-profile marker file (.setup-profile.json) instead of the
  profile.yaml role, as c3f85bc54f decided;
- the first-contact note's message argument beside main's internal flag;
- the setup-profile gate on the shared-metrics consent dialog;
- window sizing (window-growth) in place of chat-onboarding-window.

Desktop i18n follows main's per-section sibling files. The onboarding
strings go into them (modelMenu.localSetup, handoffTour.localTitle and
localText) and setupChoose/startChat stay in en.ts. guidedGreeting is
gone. The shared-metrics copy uses the branch's rewrite in English, with
the pre-opt-in sentence corrected for the fresh-install note
(d3674513c8); locales keep main's translations, with installId and
consentWindow folded into the new sending key.

The built-in tour reads local-model eligibility from the local-setup
offer store, the one place that rule lives on this branch. The gateway
contract and the onboarding skill docs pages are regenerated.
2026-10-07 03:27:01 +05:30
alt-glitch
1590669243 onboarding: leave MEDIA and the Store bundle env out of this PR
MEDIA (2f1fabd4bf, the backend MEDIA: parser and attachment parts) is
deferred to its own PR. Its 53 MEDIA-only files go back to the merge-base,
so they take main's version; the 10 files it shares with onboarding keep only
their onboarding hunks (setup_choose and start_chat cards, the start_chat
contracts, the scripted prelude, the first-contact note, the local-setup
turn report, intro_resends, visible_text, the bigger chat headings). The
generated gateway contract is regenerated after the merge with main.

The Store variant's HERMES_GUEST_ONBOARDING bundle env (39af862aec) is
reverted here; it ships through Ari's bundle work.
2026-10-07 03:07:33 +05:30
Sanjay Santhanam
1212a7f18c fix(gateway): preserve session lineage in the session.list RPC
The session.list projection dropped the durable lineage root returned by
SessionDB, so RPC consumers could not group a compressed conversation
with its continuation — REST (/api/sessions) already projected it, and
the desktop store keys pinning and lineage dedup on the field
(apps/desktop/src/store/session.ts::sessionPinId).

Forward `_lineage_root_id` through `_session_row_summary`, declare it on
the `SessionListRow` wire contract, and regenerate the TS/OpenRPC
artifacts so every consumer sees it.

Fixes #66663
Salvaged from #66675 by @Sanjays2402 (authorship preserved); adapted to
methods_session.py and extended with the wire contract + regenerated
artifacts.
2026-10-06 17:03:26 -04:00
Teknium
00adf69d92 feat(stt): live transcription while you speak on CLI, TUI and Desktop (stt.streaming)
Voice input was record-then-upload on every surface: nothing showed while the user
talked, and the transcript arrived only after the whole take uploaded. With
`stt.streaming: true` the openai, xai and elevenlabs providers now stream microphone
PCM to their realtime endpoints while the user speaks. Partial text renders live and
the final transcript is ready at end of speech.

- tools/transcription_streaming.py: one session API (push_audio / end_audio /
  finalize / cancel, an on_partial callback, a worker thread that owns the socket)
  with three wires, all verified live: the OpenAI Realtime `type: transcription`
  session (gpt-live-transcribe, 16->24 kHz resample since the endpoint refuses
  < 24 kHz), xAI wss /v1/stt with interim results, and ElevenLabs Scribe v2
  realtime. Plugin providers opt in through
  TranscriptionProvider.streaming_capable / open_stream_session.
- tools/voice_mode.py: AudioRecorder taps its capture callback into the session and
  parks it under the WAV path. transcribe_recording uses the live transcript and
  falls back to the file on any failure or empty result, so CLI push-to-talk, the
  continuous VAD loop and the TUI gateway gain streaming with no call-site changes.
- Classic CLI and Ink TUI show partial text in the input placeholder while recording
  (new `voice.partial` gateway event, contract regenerated). Desktop dictation
  streams PCM to the new /api/audio/transcribe-stream socket (provider keys stay on
  the host, and the host resamples) and shows partial text in the dictation pill.
  The recorded blob stays the fallback.

The plugin streaming contract (streaming_capable / open_stream_session /
TranscriptionStreamSession) and the transcribe-stream socket shape come from #103173.

Co-authored-by: Vinicius <timelord1025@gmail.com>
2026-10-06 12:50:31 -07:00
alt-glitch
bf81f4b2c9 fix(tour): built-in tour no longer opens over a chat the user switched to (review)
Re-check the active chat after the local-model wait and answer 'Tours only run in the session the user is looking at.' when it changed.
Move TourPreset to tools/tour_presets.py so importing the wire contracts no longer registers gui_tour.
2026-10-07 01:02:34 +05:30
alt-glitch
890976c224 feat(tour): gui_tour start with no steps runs the app's built-in tour
A general look around the desktop app is now one tool call. Before, the
model ran a targets scan and wrote its own steps from raw DOM labels: two
round trips, English-only copy.

- gui_tour action='start' without steps runs the built-in tour on
  surface='app' (refused on 'preview'). preset 'quick' shows four stops:
  chats list, composer, new session, model picker. 'full' (the default)
  adds what the interface mode puts on screen. A stop that is not on
  screen is skipped. The model stop adds a line when this computer can
  run a local model (the local-setup tip's fit check, capped at 2 s).
- TourPreset is defined once, in tools/tour_tool.py. The wire contract
  types tour.preset with it, so the renderer uses the generated
  TourPreset type. Contracts regenerated.
- The stop copy lives in i18n (appTour, en_app_tour.ts); other locales
  fall back to English until the translations PR.
- engine.ts: one shared teardown for every driver exit, which now tells
  the host. The app host clears a reactive $tourActive flag, so the
  usage-stats consent dialog waits until no tour is on screen. A closed
  one-off highlight also clears it.
- en.ts and types.ts are over the size cap, so the existing handoffTour
  copy and type move unchanged into the same siblings to offset appTour.

test_start_validates_its_steps: a bare start is no longer an error; it is
the built-in tour, and start on surface='preview' without steps is.
2026-10-07 01:02:34 +05:30
alt-glitch
901da1cb6a fix(catalog): sessions off the desktop no longer see manage_catalog through tool_search (review)
An agent stores its session platform's gated toolsets in disabled_toolsets at load, so the tool_search listing, the bridge's search, MCP refreshes and delegate children drop manage_catalog too.
InstallPhase moves to the leaf hermes_cli/plugin_install_phase.py; the gateway contracts no longer import the installer.
The per-home enable lock moves from the plugins_cmd facade to plugins_cmd_install._enable_placed.
Reusing an installed catalog tree reads the install record through catalog_install_record, so older top-level catalog_name records match.
A failed row no longer redacts the Advanced choices (profile name, commit); only credentials are redacted.
The profiles.py role comment no longer says a role grants the setup toolset.
2026-10-07 01:01:14 +05:30
alt-glitch
c2b13dc3d8 feat(desktop): every desktop chat can install catalog plugins; tools gated by session platform
Toolsets that exist only for one kind of session now say so on their spec:
`platforms=frozenset({"desktop"})`. `TOOLSET_SESSION_PLATFORMS` is derived
from the specs that carry the key, `all` / `*` / no selection skip those
toolsets, and `toolsets.agent_tool_drops(agent)` removes their tools from any
agent whose own session platform is not listed. `agent_init._load_tools` and
`tools/mcp_tool_agent.py` (MCP refresh and prefix restore) call that one
helper. `tool_search._DIRECT_SURFACE_TOOLSETS` is derived from
`CLIENT_SURFACE_TOOLSETS` and `TOOLSET_SESSION_PLATFORMS`.

This replaces the profile-role gate: `profile_role_toolsets`, the role code in
`_with_session_toolsets` and the explicit-pin branch, the strip step in
`_select_tool_names`, and main's `setup` toolset (whose only tool moves out)
are deleted. `manage_catalog` moves into a new `catalog` toolset, which the
desktop client folds into every desktop chat like `project` and `desktop_ui`.
It is deferred behind tool_search by default (`config_defaults` defer list).
The shared-metrics schemas' toolset enum follows (`setup` out, `catalog` in).

The install card works outside the setup chat (#129650, with review fixes):
- Parallel installs: `_set_plugin_enabled` holds a per-home lock around the
  read-then-admit section, so rows enabled at once no longer refuse each other.
- A catalog plugin already on disk and not enabled is enabled in place, so Try
  again after a refused enable can finish.
- A failed catalog row counts as resolved (`contract.resolves`), so it no longer
  holds the turn to the 300 s deadline. The snapshot carries `resolved`, and the
  card reads it instead of a hand copy of the backend's rule.
- Each card draws its own operation by op id, because models reuse call ids.
- The row carries the approved non-secret choices (`approved`: force, enable,
  ref) and the chosen profile, so Try again after the card settled repeats them.
- Install phases are ids (`InstallPhase`, wire `CatalogInstallPhase`) and the
  installed-row notes are fields (`enabled`, `missing_env`, `server_errors`,
  `already_installed`, `requires_hermes`); the card words them in English
  strings (translations follow later). Failure reasons stay raw text, and
  `detail` keeps the English notes for the model.
- Installs and search target the calling chat's own home, carried as a path;
  the row's profile name is only a label, with no `'default'` fallback in
  Python or TypeScript.
- `dashboard_install_plugin` is split into `_resolve_source`, `_place_tree` and
  `_enable_placed`.

Desktop English strings for the card move to `en_catalog_install.ts` /
`types_catalog_install.ts`, which offsets the growth of `en.ts` and `types.ts`.
2026-10-07 01:01:14 +05:30
Robin Fernandes
90ff655c81 feat(free-tier): browser challenge primitive for the anonymous token exchange
NAS may answer POST /api/anonymous/token with 428 challenge_required and a
portal URL. Hermes implements the one `browser-v1` primitive: open the URL
(hidden where it can), poll /api/anonymous/challenge/status, exchange again.
Everything the page does (BotID, in-browser proof of work, an interactive
Turnstile fallback, its copy) belongs to the portal and changes without a
Hermes release. The mint stays authoritative throughout.

Python (hermes_cli/anon_challenge.py)
- the exchange reports surface (desktop / cli / gateway), version and the
  browser-v1 capability; a UA of the documented shape goes on every call
- the wait runs outside the auth-store and shared-store locks, under a
  per-profile work lock; only a caller someone is waiting on waits — the
  keepalive tick and a status paint announce the challenge and return
- 403 signin_required / access_denied and any 428 this version has no
  primitive for become a terminal sign-in error, scoped to the token
  exchange; sign-up and the promotion endpoints keep their previous verdicts
- status-poll 429 / 5xx / transport are blips; a 4xx is a verdict (the
  credential is gone); a second 428 starts a fail-fast cooldown so a boot
  burst of token readers does not each re-mint and reopen a window
- the pending challenge is published as the free_tier.challenge event and
  on free_tier.status for a client that connects later; the desktop reports
  how each window ended (free_tier.challenge_result) so the waiter re-mints
  promptly, and an attempt counter lets a fresh foreground attempt reopen a
  window the user closed without a status replay reopening it
- terminal: print the link, open a graphical browser once per ticket; a
  gateway process never opens one; control characters never reach the tty
- challenge URLs are only ever opened on the portal origin the exchange hit

Desktop (apps/desktop/electron/challenge-window.ts)
- the page loads in a hidden, sandboxed window of its own partition, pinned
  to the portal origin (will-navigate, will-redirect, off-origin commits end
  the run); a phase is read only from a main-frame portal URL fragment
- #interactive reveals the window (never for an optional challenge), #done
  and #failed close it; an HTTP error commit, a load failure and a crashed
  renderer are retryable, a closed window is not; timers are bounded
- the renderer relays the event and the status read to one window per
  attempt and reports the outcome once; a backend without the RPC is fine
2026-10-06 23:14:10 +05:30
Teknium
f640a6274f fix: Desktop backend update names owed work on partial and receipt-proven runs
The backend-apply store consulted the receipt's owed steps only on exit 0. A
partial run (record_user_action turns a committed success into partial / exit
1, e.g. a stash whose restore conflicted) fell through to "Backend update
failed." and a receipt-proven partial after the restart to "did not come back
online", dropping the exact recovery instruction; and the success branch read
followups from whatever receipt the status route attached, which is the
latest receipt when this action has none.

Add @hermes/shared updateDebt(receipt, actionId): the debt of a committed
(success|partial) run, only when the receipt's action_id is the id the start
request returned. The store composes it on success and failure: follow-ups
keep the rerun remedy, a user action shows the producer's instruction verbatim
(a rerun does not restore a parked stash). The existing owed-success spec's
receipt gains the matching action_id the real route emits.
2026-10-06 03:44:49 -07:00
Teknium
b5025017d5 feat(telemetry): ask once more where a "No thanks" may never have been seen
Before the type-ahead fix, an Enter pressed while `hermes chat` started
could save "No thanks" before the offer was on screen, and every surface
then treated the profile as answered. Every answer now also writes
`telemetry.shared_metrics.offer_version`. An "off" answer without it reads
as `reask`: undecided, so the CLI, Desktop strip and dashboard banner offer
it once more with a one-line reason. Any response settles it, including
Esc in the terminal and the banner close, which keep "No thanks", so
nobody is asked a third time. Opt-in answers were always deliberate picks
and are never re-asked.
2026-10-06 00:40:30 -07:00
Teknium
1dfef400a7 fix(shared): a hidden Desktop window no longer drops its own healthy gateway socket
The gateway.ping heartbeat failed the transport when the newest inbound
frame was older than the 45 s deadline. A hidden or minimized Chromium
window runs the 15 s heartbeat interval once a minute (intensive wake-up
throttling), so at every tick the previous ping's pong is ~60 s old even
though the backend answered it within milliseconds. Every idle hidden
window closed a healthy socket, redialed, and the renderer recorded
hermes.desktop.friction kind=backend_disconnect detail=network for it.

Judge silence only when the newest ping we sent is still unanswered
(lastPingAt > lastLivenessAt). A dead transport still fails one tick
after its first unanswered ping, throttled or not; an answered one is
never dropped for timer spacing. This also covers the suspended-process
shape (OS sleep, WebView freezer, SIGSTOP): the pre-suspend ping was
answered, so the first tick after resume pings instead of failing.
2026-10-05 15:37:47 -07:00
Brooklyn Nicholson
d0e40fff3c feat(usage): report credential-pool usage per account 2026-10-05 14:02:38 -05:00
Brooklyn Nicholson
38d19b92e8 feat(desktop): a progress chip warns before a subscription hits its usage wall
The provider heading's status slot now shows a fuel gauge for the
tightest usage window: hidden while there's room, "17% left · resets
3:00 PM" at 20% or less (amber from 10%), with a bar along its bottom
edge for what's left, then the existing "Limited until" once the login
is out. Hovering lists every window (5-hour, weekly, per-family).

model.options attaches `usage: { windows }` from a per-profile cache in
agent/account_usage_cache.py and refreshes it in the background, so the
picker never waits on a usage API. Every fetch_account_usage call lands
in that cache, including the one session.usage runs after each turn, so
the chip is at most a turn or two minutes behind. Covers the built-in
fetchers (Anthropic, Codex, OpenRouter) and any provider plugin's usage
hook.

Both pickers now render one ProviderStatusChip instead of separate
limit badges.
2026-10-05 12:21:38 -05:00
Brooklyn Nicholson
8ab6d64043 feat(desktop): a rate-limited provider says why and until when in the model pickers
model.options now attaches `limit` to a row whose credential pool is
rate-limited: `account` (every live entry benched credential-wide, e.g. a
Claude Pro/Max 5-hour window) with the pool's own reset time, or `models`
(only those models are cooling down, each until its own time). Read from a
new read-only CredentialPool.limit_state; entitlement benches aren't reset
windows and stay out.

Desktop renders it in both pickers: an amber "Limited until 4:30 PM" badge
on the provider heading with its models dimmed but still selectable, or a
"resets 4:30 PM" badge on just the cooled-down row so its siblings read as
the way to keep working. A limit whose reset has passed drops on its own.
2026-10-05 12:10:01 -05:00
ethernet
3724ace0c8 Merge remote-tracking branch 'origin/main' into feat/store-guest-onboarding
# Conflicts:
#	apps/desktop/src/app/session/hooks/use-message-stream/gateway-event/message-stream.ts
#	apps/desktop/src/app/session/hooks/use-message-stream/gateway-event/types.ts
#	apps/desktop/src/app/session/hooks/use-message-stream/index.ts
2026-10-05 11:46:27 -04:00
kshitijk4poor
f33d817404 fix(agent): name a final that reuses an already-delivered response
When the model answers beside a housekeeping tool call (memory, todo_list,
skill_manage, session_search) and the follow-up is empty, the turn ends on
that pre-tool answer (fallback_prior_turn_content, since #10472). The final
then repeats text the client already streamed before the tool, but nothing on
the wire says so: message.complete looks like a new response.

Record the reuse where it happens (the housekeeping fallback and the
budget-exhaustion reuse of a previewed verification candidate), report
response_reused only when the final still equals that text (a footer or
explanation appended later drops it), and carry it on message.complete.
Never inferred from equal text: "Done." / tool / "Done." are two responses.
2026-10-04 18:04:17 -07:00
ethernet
4efdfb6d84 Merge remote-tracking branch 'origin/onboarding/next' into feat/store-guest-onboarding
# Conflicts:
#	apps/desktop/electron/window-focus-policy.ts
#	apps/desktop/src/components/assistant-ui/inline-preview-directive.tsx
#	apps/desktop/src/lib/chat-messages/parts.ts
#	apps/desktop/src/plugins/hermes-bots/types.ts
#	apps/desktop/src/sdk/index.ts
#	hermes_cli/plugins_cmd_install.py
#	hermes_cli/profile_distribution.py
#	plugins/web/keyless_mcp.py
#	tui_gateway/methods_profiles.py
#	tui_gateway/server.py
#	website/docs/developer-guide/relay-shared-metrics.md
#	website/docs/user-guide/features/web-search.md
2026-10-03 17:41:14 -04:00
FroRaut
3778021382 feat(desktop): exact subscription speed tiers (Standard / Fast / Ultrafast)
Ultrafast collapsed into ordinary Fast across the stack: the catalog only
reported `fast`, presets and drafts stored a boolean, session.create took
only `fast`, and Settings had a single Fast switch, so an Ultrafast default
read as Fast and a new chat could lose it.

- Inventory reports `ultrafast` per model from the same route-aware
  resolver that builds the request; Daybreak aliases offer no accelerated
  speed (their priority request is ignored).
- session.create accepts an exact `service_tier` that wins over the legacy
  `fast` flag; unknown tiers are rejected.
- Desktop keeps the exact tier through presets, draft creation, session
  options and labels; an unsupported remembered tier falls back to
  Standard, and failed edits reconcile to the confirmed state.
- Settings → Model → Main model offers one Speed choice (Standard / Fast /
  Ultrafast) on models that support Ultrafast; others keep the Fast switch.

Split out of #129251 (which keeps Daybreak).

(cherry picked from commit 4e332fb850c989ef9f34bd70aaca174042beecc6)
2026-10-04 02:12:57 +05:30
alt-glitch
f1c79255fe feat(plugins): requires.gpu gates a plugin server on the host's GPU vendor
A portable plugin can now declare `requires: {gpu: nvidia}` for an MCP
server. On a host without an NVIDIA GPU, Hermes then refuses the
install, hides the server's tools, and the catalog card says "needs an
NVIDIA GPU". The check is the existing
`hermes_platform.host.facts.gpu_class()` read.

The requirement is independent of `app:`. The RTX Remix Toolkit is the
first consumer: it is a package the user unpacks anywhere, so there is
no install location to check, but it needs an NVIDIA GPU.

An unreadable GPU (`unknown`) passes, so a failed registry or sysfs
read does not lock out a machine that has the GPU. Only `nvidia` is
accepted: gpu_class() reports the highest-priority vendor, which
answers the question exactly for NVIDIA and not for AMD or Intel.

A registered server on such a host reports the new `unsupported_gpu`
state, carried through the gateway contract and the Plugins tab, with
a sentence that names the GPU instead of "Install <app>".
2026-10-03 10:26:12 -07:00
kshitijk4poor
370b3a91c8 fix(tui_gateway): type cron last_delivery_unverified as a list of targets
The scheduler persists last_delivery_unverified as a list of unconfirmed
platform:chat_id[:thread_id] targets and cron.manage passes it through, but
CronJobRow typed it bool | None, so contract validation rejected any job
with an unconfirmed delivery. Regenerate the TS/OpenRPC contract.
2026-10-03 22:23:27 +05:30
teknium1
c55a6e3d9f feat(desktop): identity-attribution traces for multi-connection bot rosters
Change-only desktop.log / agent.log lines that name which writer moved a
bot identity when two connections share a profile name:

- [gateway-route] per (connection, profile): via=primary|secondary and the
  window primary; `active` on every active-scope change (key, socket
  connection, profile) and `published` on every published-connection
  change, so a socket/label disagreement shows up side by side.
- [bot-roster] each Bots-pane snapshot: the install that ANSWERED
  profiles.list vs the install expected for activeConnectionId;
  MISROUTED when they differ.
- [bot-relay] rows per connection with the answering install; MISROUTED
  when two connections answer from the same install.
- [bot-meta] every $botMeta title change per scoped key.
- backend: bot_relay.roster.sync and ui_meta['hermes-bots'].title writes.

profiles.list now echoes install_id (same value as /api/status) so a
client can prove which machine answered a routed list. Additive field;
older clients ignore it. Plugins reach the tracer through
host.traceIdentityChange?.() so wholesale SDK mocks stay valid.
2026-10-02 21:11:01 -07:00
teknium1
83097ba008 fix(desktop): keep the hermes://skill/install deep link through the catalog revert
The plugin deep link (hermes://plugin/install?catalog=) and the skill
deep link are separate features: one per kind. Reverting #119381's card
catalog should not leave skills without a link, so this restores the
skill route on its own:

- Desktop: skill-deeplink-install store, the skill route in
  deeplink-routes, its use-desktop-integrations wiring, and the
  confirm-store onConfirm/details/busy/done phases its dialog uses
  (restored from main with their tests). Its nine strings move from the
  deleted catalog.* namespace to skillDeepLink.* (en + the six locales
  that translated them).
- Website: Skills Hub "Install in Hermes" button, plus the
  source-qualified installIdentifier from extract-skills.py so the link,
  the embedded picker and the CLI command all name the exact skill
  (apps/shared/src/catalog-install.ts keeps only the skill helpers).
- Docs: skills.md "Install from the website" section, desktop.md pointer.

The card catalog UI stays reverted.
2026-10-02 10:29:28 -07:00
teknium1
6cb7a64e59 revert: Skills and Plugins card catalog browser, third landing (reverts #119381)
Reverts all seven commits of #119381 (59c54c78..476ff4dd), which
re-landed the native card-based Skills/Plugins catalog browser after it
was reverted twice (#113037 reverting #112669, #115787 reverting
#113128). Capabilities -> Skills and Plugins return to the list/detail
tabs with the embedded catalog picker they had before #119381.

Removed with it:
- Desktop catalog/ module (browser, cards, detail dialog, filters,
  reels/masonry, install switch) and skill deep-link install
  (hermes://skill/install); skills/embedded-hub-picker.tsx comes back.
- apps/shared catalog-browse.ts and catalog-install.ts.
- The `catalog` i18n namespace in every locale (+ regenerated
  locales/_keys.desktop.json); the two i18n pack tests and the keys
  test now exercise connectorsPage.searchPlaceholder instead.
- Website /skills "Open in Hermes Desktop" skill links and the
  extract-skills install identifiers; docs return to the pre-#119381 text.
- Later fixes that only patched the reverted catalog code go with it:
  5832a6eb93 + 7aba2cbbe9 (catalog download deadline),
  9e63cdc4b8 (catalog lookalike rows), c756986db2 (catalog counts),
  the catalog half of c13232e90c / f265273347.

Kept on purpose (later work that depends on code #119381 introduced):
- DialogContent `chrome` / `overlayClassName` / nested-dialog layering
  (the image lightbox uses them).
- vitest.setup.ts / src/test/jsdom.ts stubs (other suites rely on them).
- Kanban toolset wiring on the Plugins Desktop switch (cd30fdadf6,
  7ca2f5665f) and its tests, the `hermes_not_connected` server tone,
  the SearchField flex-1 fix, skill provenance helpers. SkillDetail shows
  a provenance pill only for agent/hub skills, since the pre-#119381
  labels have no `external` entry.
2026-10-02 10:29:28 -07:00
Brooklyn Nicholson
5654863ccd fix(desktop,tui): /browser use [off] works outside the CLI
The Desktop and TUI subcommand pickers offer /browser use (it is in the
registry's subcommands), but both handlers only knew connect, disconnect
and status, so picking it printed the usage line.

browser.manage gains a use action (enabled: bool) that writes the session
profile's browser.backend through the same set_browser_use_mode helper the
CLI now calls, and status/use report browser_use. The handler is profile
scoped, so a worker session writes its own profile. A live agent keeps its
tools (prompt cache), so Desktop and TUI say the switch applies to new
chats instead of swapping tools mid-conversation. use and status also run
on remote backends; connect/disconnect stay local-only.
2026-10-02 11:24:58 -05:00
teknium1
eb049af69c Revert "feat(desktop): copy and Bot Mode group turns follow no-deadline prompts"
This reverts commit 0ea07255e0.
2026-10-01 13:23:12 -07:00
Austin Pickett
666b9f04c4 fix(shared): never dispatch a truncated reconnect replay tail
session.events.since reports truncated=true when the ring evicted events
between the client's watermark and its oldest retained seq. The client
ignored the flag and dispatched the retained tail as a gap-free replay,
painting responses missing their prefix, tool state or terminal event.

On truncation, skip the window up to the server's head without
dispatching it. The session's replay barrier still resolves true, so the
reconnect backstop's history read (#94779) resyncs from the stored
transcript; parked live frames newer than the head still dispatch.

Covers the client half of #100122; the server-side session-eviction
seq reset is #124587.
2026-10-01 09:36:23 -04:00
Brooklyn Nicholson
c579be967f fix(tui_gateway,desktop): every branch create accepts the idempotency key, and a branch_stored hit keeps its messages off the wire
The retry notification added in this PR re-invokes forkBranch with the same
key, but forkBranch stamps idempotency_key into the params of EVERY branch
flavor — including session.branch_stored, whose params contract is
extra=forbid and did not declare the key. The desktop's right-click branch
(that spec's exact path) answered 4000 'idempotency_key: Extra inputs are
not permitted', no child row was created, and e2e/core/lineage-sidebar.spec.ts
failed at 'the branch created a session row' (0 rows, 60s poll).

- SessionBranchStoredParams: accept idempotency_key (optional; an older
  client that omits it is unchanged).
- session.branch / session.branch_whole: accept idempotency_key and dedupe
  via the same _idempotency_keys registry as session.create, so a
  lost-response retry of a LIVE branch also returns the SAME child instead
  of a duplicate (the desktop retry covers those flavors too, but the key
  never reached the backend).
- _build_branch_agent records parent_session_id/branch_title on the child
  runtime so the idempotent hit answers the same parent/title as the fresh
  reply, without a DB round-trip.
- The session.create idempotent-hit early return now honors
  copy_parent_history: a retried session.branch_stored answers
  messages_omitted=true and never puts the copied transcript on the wire
  (its result contract forbids 'messages', and re-serializing the parent
  transcript through the renderer is what branch_stored exists to avoid).
- Regenerated gateway-contract artifacts; two use-session-actions tests
  updated to expect the (opaque) key in branch params.

Red on main: tests/tui_gateway/test_protocol.py::
test_session_branch_stored_accepts_idempotency_key fails with the exact
4000 the e2e saw. Green with the fix, plus
test_session_branch_idempotency_key_dedupes_retry (server suite).
e2e lineage-sidebar passes locally on macOS (14-19s; CI PR run failed at 1.2m).
2026-10-01 05:54:49 -05:00
jinglun010-cpu
4d8c5922eb fix: address sweeper review on #65411 — notifyError path + idempotent retry
Two issues from the hermes-sweeper review:

1. Notification bypassed readableError normalization. The catch block
   called notify() with raw err.message, skipping wrapper stripping,
   detail extraction, and the >180-char long-message fallback. Switch
   to notifyError(err, fallback, action) so the retry action rides the
   established normalization path. notifyError gains an optional third
   action parameter (backward compatible).

2. Retry was unsafe against a lost response. tui_gateway/server.py
   session.create generates a fresh UUID per call; if the first create
   committed but its response was lost in transit, the user's retry
   would spawn a duplicate child session. Add optional idempotency_key
   support: the server dedupes on (key, live session), returning the
   original sid for a retry, falling through to a fresh create if the
   original session was closed in between. forkBranch generates a
   stable key per logical branch attempt and passes it through the
   retry action so both attempts share it.

Tests:
- tests/tui_gateway/test_protocol.py: 3 new tests cover dedupe, no-key
  backward compat, and key expiry when the session is closed. Full
  protocol file: 86/86 passed.
- use-session-actions.test.tsx: 2 new tests cover key reuse across
  retry and readableError normalization on the failure notification.
  Full file: 21/21 passed. tsc --noEmit: 0 errors.
- scripts/check-windows-footguns.py on changed files: clean.
2026-10-01 05:54:49 -05:00
Brooklyn Nicholson
0ea07255e0 feat(desktop): copy and Bot Mode group turns follow no-deadline prompts
A Bot Mode group turn blocked on the user's clarify/approval answer pushes its
runaway hard cap out instead of expiring the card under the room; the offline
toast and the Settings description stop promising an approval timeout; the
approval.respond deadline comments describe what it bounds now.
2026-10-01 03:50:55 -05:00
Siddharth Balyan
a2065efd51 fix(setup): each setup card answers only its own call (#130062)
One Skip broke a tester's whole onboarding. The provider named every
tool call "call_0", and the desktop bound each setup card to its
request by call id alone. After the apps card was skipped, every later
card with "call_0" showed that old "Skipped" answer instead of its
picker. Composer text was also saved as a pick, so "hey" became a
plugin and a complaint became the layout.

- The agent renames a tool call whose id is already used earlier in
  the session (call_0 -> call_0_d2), before the call is stored or run.
  Ids already in history never change, so the prompt cache holds.
- The card's request binding is keyed by session, message and call id.
- Composer text on a card with fixed rows counts as a pick only when it
  names a row by id or label. Otherwise setup_choose returns
  outcome "typed" with the words in `said`, records no card facts, and
  tells the model to reply and send the card again. The settled card
  shows the typed words.
2026-10-01 12:44:40 +05:30
Brooklyn Nicholson
036c577c86 fix(desktop): surface staged skill-write review on the desktop and TUI
With skills.write_approval on, every skill write is staged to the
file-backed pending store, but no non-CLI surface could review it:
the desktop resolver marked the whole /skills family as the sidebar's
(desktop="settings") and discarded the subcommand token, and
tui_gateway command.dispatch had no route for /skills or /memory —
a failed slash worker turned both into the routing refusal, so staged
writes accumulated silently (#98330, #123315, #118812).

- hermes_cli/commands.py: new CommandDef.desktop_subcommands field;
  /skills declares its write-approval review slice
  (pending/approve/reject/diff/approval) and drops desktop="settings".
  command_desktop_meta serializes the allowlist (empty tuple = deny
  all); gateway contract regenerated.
- apps/desktop: the resolver threads the typed arg —
  desktopSubcommandAllowlist, an exec gate
  (desktopSubcommandUnavailableMessage) that renders an actionable
  refusal for hub mutations instead of forwarding them, completion
  filtering so the popover never suggests a dead end, and the
  isDesktopSlashCommand(name, arg) threading at the runExec gate.
  An explicit /skills spec keeps a current Desktop able to review
  staged writes even against an older backend catalog.
- tui_gateway command.dispatch: _cmd_skills answers only the
  registry-declared review slice under _session_home_scope (an
  unlisted subcommand keeps the 4018 routing refusal); _cmd_memory
  routes the shared handler with the live agent's store or a fresh
  on-disk one and a profile-scoped config setter. Gate-off still
  answers while staged writes exist (gateway parity, never stranded).
- tools/write_approval: _staged()'s review hint is surface-aware —
  slash-capable surfaces (CLI/TUI/desktop/chat gateway) keep
  "<command> pending"; headless contexts (cron, kanban, api_server,
  webhook delivery) get the pending directory path instead of a
  command nobody can deliver.

Batch skill writes also rendered "(batch on ...)" for /skills diff —
skill_pending_diff had no action='batch' case; ops now diff in order
against what earlier ops of the same batch left (finn763's #123861).

Supersedes #98794 (registry-driven desktop_subcommands + resolver
allowlist), #105773, #118821 (command.dispatch handlers + arg
threading), #123861 (batch diff renderer). #107799 is superseded by
the same subcommand-aware resolution.

Co-authored-by: foma-agent <foma@bokonon.ai>
Co-authored-by: Jason Havenaar <35441875+jasonHav@users.noreply.github.com>
Co-authored-by: finn763 <165816600+finn763@users.noreply.github.com>
Co-authored-by: chelsealong <chelsealong@126.com>
2026-09-30 23:05:43 -05:00
Siddharth Balyan
59cbf85cca Setup cards name the pick, skip an empty apps or plugins list, and wait for the intro (#129383)
* fix: setup cards tell the model the name the user saw, and an empty apps or plugins list shows no card

The accent card showed "Ultraviolet", but the result only carried "#8a2be2", so
the model guessed a CSS name ("BlueViolet, done."). The card now answers with the
label of each picked row next to its id, the backend passes it through for the
app-owned lists it cannot name itself (accent, theme, layout, connectors,
plugins), and the tool description tells the model to say the label.

A session with no Nous identity (own key, guest sign-up rate limited at boot)
got an apps card that said "This list isn't available right now". Now
setup_choose kind:"connectors" checks connectors_available() in the setup
session's own profile scope before any card: with no identity and the free tier
on, it makes one guest sign-up attempt through ensure_portal_identity (the mint
memo's cooldown still applies), and if connectors are still closed it returns
no_answer with a one-line notice and the next card, without showing a card. An
empty plugins list returns the same way. The skill says one line about the free
Nous account and moves on.

The renderer re-reads a cached "unavailable" connector catalog when the card
mounts, so a guest minted by the card is not hidden behind the prefetch taken at
the start of setup.

* fix: the setup intro holds the thread until its spring settles

The backend prelude writes the first assistant row and the name card while the
intro copy is still typing. The copy's cover uses the chat surface colour, which
is transparent under glass, so the card showed through mid-motion. While the copy
plays ($introHoldsThread, true only for the 'playing' stage), the thread under it
is hidden; it shows when the spring settles, and at once with reduced motion
(the stage goes straight to 'landed'). The backend prelude is unchanged: its
tool call waits for the answer.
2026-09-30 20:49:25 +05:30
Brooklyn Nicholson
fe5b728bd9 fix(bots): size live_message_count with SessionDB's own display projection
The count left out DISPLAY_VISIBLE_SQL, so model-only rows from
micro-compaction were counted while the reader painted none, and the history
wait this count exists to skip still ran. SessionDB.display_message_count now
counts the same set _display_rows_from_conn paints, through _read_one instead
of reaching into db._lock/_conn, and one _live_count_field serves all three
listing paths. The field is declared on SessionListRow, ProfileSessionPreview
and ProfileCanonicalSession (extra=forbid) and the desktop derives it from the
generated contract instead of hand-typing it twice.
2026-09-30 10:12:37 -05:00
Siddharth Balyan
fe50fc8b60 Setup cards: tour and fork always arrive with their rows, apps preselected from the scan, honest skill (#129344)
* feat(setup): the backend fills the tour and fork cards and preselects scanned apps

The welcome model fills only the required parameters of setup_choose, so
cards that needed options arrived as bare text boxes in play 4. options is
now required ([] means the app's list, or free text for a question).

Two new kinds, tour and fork, take their rows from the backend whatever
the model sends. The fork rows come from the facts the /initiate-setup turn
embedded, recorded next to the setup state; "Something else" on a
machine-first fork opens the rest in the same call. The connectors and
plugins cards start with the apps the scan saw in use, and Blender when it
is here, already picked. The result names a picked row (label), so the
settled card shows labels for rows that are not in the call's args.

* fix(setup): the setup skill answers honestly about the scan and always reaches a handoff

Card lines carry options and use the app-owned tour and fork kinds. When
asked what Hermes knows, the model answers from the facts and names the
scan instead of denying it. A skipped fork goes to a first-task card built
from the scan, a second skip offers one concrete task, and a card left for
an off-flow question is re-sent. Dead fork-building and fallback text is
cut, which keeps the skill under 25 KB.

* fix(setup): card facts per setup conversation; the backend steers each beat and carries the handoff

The saved card facts were one file per setup profile, so a second
/initiate-setup overwrote the first conversation's fork rows. They are now
keyed by the session id under the bound Hermes home, and setup_choose reads
its own session's block. A fork card with no recorded facts (skill loaded
through the inline-shell hook) collects them fresh.

setup_choose now returns `next` for the flow's following step (the next
card's exact arguments, the skip ladder from the fork on, a re-send of the
pending card after typed off-flow text) and, from the fork, `handoff`: the
message parts and the plan, cut to this machine, from templates/handoff.md.
That moves about 6 KB of handoff and plan text out of the first turn.

Rows the model sends for tour, fork and the new machine_use kind are
dropped before validation, so a malformed copy can no longer fail the card.
The connectors and plugins cards are always multi-select, so preselected
rows are never lost to a single-select confirm. The off-desktop voice rule
is back in the skill.

* docs: regenerate the initiate-setup skill page

* feat(initiate-setup): one "I noticed" line before the fork shapes the first tasks
2026-09-30 20:07:55 +05:30
Siddharth Balyan
9756ef269c P18b: first-run intro — boot into the setup chat, hidden /initiate-setup, leave/Skip, Reset onboarding (#129318)
* P18b: the first-run intro boots into the setup chat and sends /initiate-setup hidden

- The kickoff stops seeding the old runbook. When ensure_setup_session reports an
  empty setup chat, the app sends /initiate-setup through the normal slash-to-skill
  path with a hidden user row, so the thread shows nothing for it, live or after a reload.
- /initiate-setup rows project to '/initiate-setup' (backend and the shared TS twin),
  not the 36k skill body.
- Intro view state in memory (off | starting | intro | ended). The boot overlay is the
  starting screen. The unlisted 'demo' layout (chat only, narrower column, minimal
  composer, no status bar) is applied while layout persistence is held, so it is never
  saved as the user's layout.
- The intro copy types centred, holds, springs up and gives way to the backend's first
  assistant row (same words). Reduced motion jumps to the end.
- Leave actions (sidebar, another chat, profile, layout pick, Cmd-K command) end the
  intro, grow the window, restore the layout, mark seen and send new chats back to the
  launch profile. The intro pauses tips and holds the provider picker and free-tier screen.
- Skip switches to the launch profile, marks seen and retires the tutorial and
  local-model tips.
- A setup start_chat success ends the guided phase (free-tier chip, consent, completion
  metric) and shows the handoff tour when tours are on.
- Settings > Advanced > Developer: Reset onboarding (reset RPC, then reload).

* P18b fixes: keep a layout picked in the intro, main window only, relaunch drives a cut-off opening

- A layout pick (card, apply_layout, picker) ends the intro on the picked layout and saves it.
- Leaving pins new chats to the launch profile by name, not to the setup profile.
- Only the main window runs the intro: not peers (Cmd-Shift-N) or watch windows.
- The window takes the demo shape only after setup.status confirms inference.
- starting has a 45 s deadline that records a failed start; a late kickoff undoes itself.
- A relaunch whose setup chat has no live turn and no model words after the last answer
  sends /initiate-setup again (hidden).
- The setup chat is a normal chat once the intro has ended.
- The demo flag lives in store/onboarding-intro.ts so the shell does not import layout code.
- Reset onboarding goes through the ambient gateway requester and brings the tips back.
- The describe_skill_invocation backend hunk stays with P18a.
- Delete SETUP_CHAT_TITLE and the guidedGreeting strings.

* fix: keep the merged /initiate-setup row display from the prelude PR
2026-09-30 20:03:22 +05:30
Siddharth Balyan
f7b0a2f613 Setup cards: pills only, no key letters, no System tile; handoff card title + Retry (#129243)
* fix(setup): pills are the only question layout, no key letters on setup pickers, no System theme tile

The focused pill shows its detail as one line under the pills. Setup cards no
longer bind letter or digit shortcuts; arrows and Enter still work. clarify
cards keep their key badges.

* feat(setup): handoff card shows the sidebar title and retries a failed start_chat

The card title follows the new chat's sidebar row. A rejected start_chat shows
"Couldn't start that chat." with Retry, which runs the same request again
through the new session.start_chat RPC from the calling session.

* fix(setup): Retry on the handoff card is durable, told to the model, and offered only when it can help

- start_chat rejections say whether the same arguments can succeed (retryable); the card offers Retry only then,
  only while the chat is idle, and not once a later start_chat in the chat started.
- session.start_chat takes the rejected call's tool_call_id and needs an explicit session: a started retry is
  recorded on that tool row's display_metadata (the model's replayed row is unchanged), a second Retry returns it,
  a reload shows it, and the next user turn tells the model once (the reactions pattern).
- A chat that started but could not open toasts "Couldn't open the chat", not "Couldn't start that chat."
- The theme card drops System whoever supplies its options.
- Question pills: the cursor follows focus and hover, pills with a detail carry a dot, the detail line has a title.
2026-09-30 18:20:12 +05:30
Siddharth Balyan
2f1fabd4bf P24 part 1: MEDIA: parsed once in the backend; ::preview deleted (#129257)
* MEDIA: tags parsed once, in the backend; clients get clean text plus attachments

tui_gateway runs the gateway's extract_media on the live stream (a tag's
line is held until it completes, then the file goes out on that delta),
on message.interim/message.complete, and on history through the shared
history projection (session.resume and the REST messages route). The wire
contract gets an attachments list on message.delta/interim/complete,
TranscriptMessage and InflightTurn.

Desktop renders attachments as data parts (media player or file card, HTML
included) and in group rooms; the TUI lists them as link rows. The three
renderer MEDIA: parsers (chat parts, speech text, Artifacts) and the TUI
MEDIA line parser are deleted. Stored rows keep their raw tags.

* Delete the ::preview directive and its prompt text

The inline HTML widget, its registration and its hidden-turn talk-back go;
an HTML file delivered with MEDIA: is a normal file card with preview. The
desktop and TUI platform hints now describe attachments. The
transcript.directives area stays for ::onboarding and ::ask.

* MEDIA stream splitter works per line; group mirrors and sync keep files; TUI shows a file row when its line completes

- MediaStreamSplitter splits each completed line once, carrying code-fence state, so
  prose that says "media:", blank-line runs and [[audio_as_voice]] no longer stall
  the stream, and cost stays linear in reply length.
- The partial-tag hold applies only at a token start (or to an upper-case prefix),
  so words like "stream" are not held.
- Subagent child mirrors stream through a splitter too, flushed at tool and end.
- Group rooms: externally written rows and the ui_meta sync copy carry attachments;
  the echo guard compares files as well as text.
- TUI: a delta that brings a new file seals the text so far with its file row.
2026-09-30 18:17:57 +05:30
Siddharth Balyan
8feae75347 Apps and plugins: drop setup_choose intent, manage_catalog out of setup, cards set up later (#129245)
* fix(setup): manage_catalog leaves the setup chat; installs go to the caller's profile

- The setup profile disables the catalog toolset. The desktop session
  folded it in as a client-surface toolset, so manage_catalog was still on
  the setup chat's eager tool list.
- Delete default_target_profile: a catalog install and search target the
  calling chat's own profile, the setup profile included.
- /initiate-setup still needs "the launch profile when run from the setup
  home"; that moves to hermes_cli.setup_profile.primary_profile().
- The manage_catalog description and the reference docs drop the setup
  chat exception.

* feat(setup): drop setup_choose intent; apps and plugins cards say they set up later

- setup_choose loses `intent` (the now / when needed / just note it choice)
  in the tool, the wire contract and the cards. The result is
  {outcome, picked}. Contracts regenerated.
- The apps and plugins pickers carry one footnote, "We'll set these up
  when you start.", replacing the plugins-only "Nothing connects or
  installs yet" note. Other locales translated; the desktop key list is
  regenerated (it was also behind en.ts on appTour and sharedMetrics).

* fix(desktop): an install card stays drawn when a connect card follows it

The renderer keeps one connection operation per session. When a turn asks
twice (a manage_catalog install card, then a manage_connections connect
card), the second operation replaced the first in the store, and the
settled install card fell back to a plain tool row.

A settled operation that the next one replaces now moves to a store keyed
by its tool call, and the catalog and connector cards read their own row's
operation from there. Both cards stay stacked in the turn.

* fix(setup): setup_choose inline calls stop passing intent; /initiate-setup reads the launch home at call time

The inline executor still mapped intent into setup_choose_tool, which no
longer takes it, so every desktop setup_choose call raised TypeError.
primary_profile now gets server._launch_home() instead of the import-time
_hermes_home.

* fix(desktop): keep one card per kind: the latest install card and the latest connect card

The replaced-operation cache kept every settled operation by tool call, so
consecutive manage_connections calls drew one connect card each. It now
keeps, per session, only the settled operation of the other kind (install
vs connect) that the current one replaced. An install card and a connect
card stay stacked in either order, and a later call of the same kind still
takes over its card. clearConnectionRequest clears both caches, and the MCP
setup card reads the same per-row selector, so sessionConnectionRequest is
gone.
2026-09-30 17:48:37 +05:30
Siddharth Balyan
90c5e8c7ab fix(desktop): offer usage stats only after onboarding, in plain words (#129140)
The usage-stats offer read the focused profile's consent, so it showed in
the setup chat (the hermes-setup profile) and again in the first task chat.
The setup profile's answer counts for nobody.

onboarding.state now reports the setup profile's name. The consent host
waits until guided onboarding is settled (the backend's intro is read and
the phase is not pending, guided or handoff; skip and handoff both mark the
intro seen) and never reads consent while the setup profile is focused.

The copy drops the jargon (bounded counters, consent window, install ID):
a short strip, a 'Usage stats' details dialog with a collapsed 'What is
counted' list checked against the shared-metrics contract, one paragraph
on what is sent, and matching Settings text. Other locales keep their old
text under the renamed keys.
2026-09-30 14:40:37 +05:30
Siddharth Balyan
c600c70dd0 One-call built-in tour for the setup chat (#129137)
* feat(tour): gui_tour start with no steps runs the app's built-in tour

The app owns the stops and their copy. preset quick shows the three
essentials; full adds the model picker and what the interface mode shows.
The model picker step says this computer can run a local model when the
local-setup tip's fit check says so.

* docs(setup): the tour beat is one gui_tour call

Quick tour and Show me everything map to preset quick and full. targets stays
for a custom tour of one thing the user asks about.

* fix(setup): /initiate-setup fills the host facts hook in place

The builder read SKILL.md raw, so the prompt kept the literal inline-shell line
and carried the host facts again in the session block. It now replaces the hook
line with the facts it already collects in-process, and the session block holds
only session facts.
2026-09-30 14:36:14 +05:30
alt-glitch
81c53411bf fix(onboarding): the kickoff seeds the one setup chat through onboarding.ensure_setup_session
Check follow-ups.

- onboarding.ensure_setup_session is back. It creates or finds the
  setup chat by title, so every call returns the same chat and whether
  it is empty. It takes optional seed messages and writes them only
  into an empty chat.
- The kickoff now seeds through it instead of its own titled
  session.create, and adopts a title hit only when it has messages. An
  empty row no longer breaks the title lookup; the kickoff seeds it.
- The setup config no longer lists "start_chat" in
  platform_toolsets.cli. The toolset does not exist on this branch, so
  config validation and CLI startup warned about an unknown toolset.
2026-09-30 11:59:42 +05:30
alt-glitch
e194c25a06 fix(onboarding): the setup profile pins its own tool limits; guide readers wait for the state read
Review follow-ups.

- The setup config now writes agent.disabled_toolsets with "catalog"
  (merged with the cloned list), so the client-surface catalog toolset
  does not fold into the setup chat once it exists.
- It pins agent.coding_context to "off", so a cloned "focus" posture
  cannot swap in the coding toolset in a code workspace.
- It replaces tools.tool_search instead of merging into the cloned
  block. A cloned "enabled: off" no longer turns the defer list off,
  and the bool form no longer raises and deletes the new profile.
- onboarding.ensure_setup_session is removed. Nothing called it, and
  its empty row would break the kickoff's title lookup. It returns
  with the intro view, which can seed an existing chat.
- requestDesktopOnboarding and the free-tier ready screen wait until
  the gate has read onboarding.state, so they no longer see "idle"
  before the guide starts.
2026-09-30 11:59:42 +05:30
alt-glitch
d8ddf55022 feat(onboarding): the backend owns onboarding state and the setup profile carries only the beat tools
The setup profile's marker now holds the onboarding state:
{intro: unseen|seen, failed_starts: 0..3, completed_at?}. New RPCs read
and change it: onboarding.state (plus eligible, read from
HERMES_GUEST_ONBOARDING only), onboarding.record_failed_start (the third
failure marks the intro seen) and onboarding.mark_seen.
onboarding.reset_setup_profile clears it. onboarding.ensure_setup_session
creates or finds the setup chat by title and writes its row at once, so
every call returns the same chat. The renderer does not call it yet; the
kickoff keeps its seeded create until the intro view lands.

The setup profile's config.yaml is now written, not appended to:
platform_toolsets.cli = [setup, start_chat, no_mcp], a hand-written
tools.tool_search.defer list that keeps apply_layout, setup_choose and
start_chat eager, and display.show_reasoning = false. Creation and reset
no longer copy MEMORY.md and USER.md from the launch profile, because
the built-in store loads them even with the memory toolset off.

The renderer no longer keeps the phase in localStorage. The gate seeds
from onboarding.state, Skip and the app handoff mark the intro seen, and
a failed start is counted instead of saved as "skipped" forever. A start
without inference turns the guide off for this launch and counts
nothing. The duplicate PERSONA prompt block is gone; SOUL.md is the only
copy.
2026-09-30 11:59:42 +05:30
alt-glitch
65d285c0a6 fix(setup): list setup_choose in the metrics schema; drop the new contract comment and doc 2026-09-30 11:59:14 +05:30