16 Commits
Author SHA1 Message Date
Adolan cb8473e446 fix(error-surface): name the provider by its display label on every error card 2026-09-27 13:19:41 -05:00
teknium1 7c2b81d320 chore: merge origin/main (resolve apps/desktop/src/components/assistant-ui/thread/assistant-message.tsx, website/docs/user-guide/desktop.md) 2026-09-19 10:54:43 -07:00
teknium1 1fc8887c6e fix(desktop): the error card names a WAF block and the User-Agent fix instead of 'retry in a moment'
upstream_blocked had no ERROR_CODE_KEYS entry or errorCodes copy, so the Desktop card fell
back to the provider-layer 'retry' body — wrong for a block a retry can never heal. Same
guidance as the CLI/TUI copy: set a User-Agent via the provider's extra_headers, or switch
provider. Other locales carry no errorCodes table and fall back to en. The Python fallback
set agent/error_surface.py::_NON_RETRYABLE_REASONS learns the reason too.
2026-09-19 09:57:21 -07:00
teknium1 645e9298b6 feat(error-surface): 429 error card shows when the usage limit resets
A "HTTP 429: The usage limit has been reached" turn offered only Retry and
never said when a retry would work, so users guessed or babysat the app
(#98852). The provider already tells us: Retry-After / resets_at /
retry_after are parsed into the turn's error context (extract_api_error_context)
and honoured by the backoff, but the datum died there.

- agent/turn_recovery.py::_stamp_limit_reset: both terminal paths
  (max_retries_exhausted_result, nonretryable_client_error_result) stamp
  failure_resets_at (epoch s) on the failed result and append one plain line
  ("Limit resets at 14:05 (in 1h 00m).") to final_response, which every text
  surface (CLI, Ink TUI, messaging gateway) renders.
- agent/error_surface.py: result path forwards failure_resets_at as
  surface.resets_at; the exception path derives it from the same context.
- tui_gateway/contracts/events.py::ErrorSurface.resets_at + regenerated
  apps/shared gateway-contract outputs.
- apps/desktop lib/error-surface.ts: parse resets_at -> resetsAt,
  formatLimitReset("HH:mm (in 1h 05m)", null once passed), diagnostics line;
  the error card renders "Limit resets at …" next to Retry (i18n copy in every
  full locale).
- Docs: website/docs/user-guide/desktop.md error-card section.

Informational only: no scheduled or automatic retry is added — firing a turn
unattended on a subscription is the maintainer's call (#98872, #103048).
2026-09-19 01:34:01 -07:00
kshitijk4poor 564687b113 fix(nous): classify the desktop's escaped-exception card with the request credential
build_error_surface_from_exception classified without the credential, so an anonymous
session's gate refusal that escaped the turn loop read as a retryable rate limit. Thread
api_key from the TUI gateway's terminal-error path. Provider-gate the named welcome-host
400 like the 403 branch, and drop the provider conjunct the anonymous gate already implies.
2026-09-17 12:28:25 +05:30
Robin FernandesandClaude Fable 5.1 16def7b8cc fix(free-tier): the desktop renders a free-tier refusal as its own card, not an OAuth re-login
A welcome-tier 403 classifies as auth_permanent, so the desktop's error
surface mapped it to "Your Nous Portal sign-in expired" with a Nous Portal
re-login button — the chat sentence never reached the user. Terminal results
on the free route now carry a structured free_tier block (kind + the chat
sentence); agent/error_surface.py turns it into a free_tier_<kind> code on
the provider layer with the sentence as `message`. The desktop gives those
codes their own titles, shows the backend sentence as the body, and offers
"Sign in with a Nous account" (the free-tier dialog) instead of the OAuth
re-login, with Retry only where a later send can succeed.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-15 20:44:42 +05:30
teknium1 23036e20a6 fix(ux): plain-language, actionable user-facing messages (core)
Squashed integration of the user-facing message audit for this surface set.
Full per-finding receipts: /tmp/ux-audit/lanes/*-receipt.md (campaign artifacts).
2026-09-15 04:12:13 -07:00
Teknium 97ca90f184 fix(desktop): expired OAuth grant shows a one-click 'Sign in again' instead of a retryable Provider error
A rejected OAuth token (HTTP 401 'User not found' from Nous Portal, Codex,
xAI…) reached the desktop error card as 'Provider error' with Retry as the
first action, which just replays the same dead credential.

Backend: nonretryable_client_error_result dropped failure_reason /
failure_retryable, so error_surface classified every non-retryable 4xx as a
retryable provider failure. It now stamps the classifier verdict like the
max-retries path, and auth-layer descriptors carry auth_kind
(oauth|api_key, derived from the provider catalog tab) + provider_label.

Desktop: an auth/oauth surface renders 'Authentication error', explains that
the <provider> sign-in expired/was revoked, and offers 'Sign in to <provider>
again' which launches that provider's existing onboarding OAuth flow scoped
to the failed session's gateway profile. Retry stays as the follow-up click.
Re-login to the provider already in use keeps the current model instead of
swapping in the recommended default.
2026-09-09 16:30:24 -07:00
Teknium 2776813df3 compat(plugins): temporary import-path shims for external plugins — ONE commit, revert on schedule
The Sep 2026 decomposition (PR #102117) makes internal import paths a non-API: names now live in
the focused modules that define them. This commit is the ONLY thing keeping the old paths alive,
so external plugins have time to update. It is deliberately a single, unsquashed commit:

    git revert <this sha>

removes every shim, stub and manifest at once on the announced date. Nothing in-tree may depend on
these pointers: scripts/check_compat_pointers.py (wired into lint.yml) fails CI if it does.

What it adds (see COMPAT_MANIFEST.md, compat_manifest.json):
- 332 facade modules get one delimited `PLUGIN-COMPAT` block appended at the end of the file
- 1,172 moved names resolved lazily via a module `__getattr__` (PEP 562) — never a top-level import,
  so no import cycles; facades that already had `__getattr__` get a chained one
- 592 third-party/stdlib names the old modules used to expose, with their original import statements
- 266 public definitions that had been deleted as unused, restored byte-for-byte from the pre-decomposition
  tree (+40 private helpers and 16 imports pulled in only because a restored definition needs them)
- 3 deleted modules recreated as re-export stubs (gateway/startup_watchdog, hermes_cli/observability/
  relay_runtime, tools/environments/modal_utils)
- private names (`_x`) get no pointer: they were never API (3,792 skipped)

Verified: all 335 touched modules import under a fresh HERMES_HOME and every manifest name resolves;
the lint reports zero in-tree uses; ruff clean; targeted suites unchanged.
2026-09-03 17:13:22 -07:00
Teknium 53db597201 simplify(compat): hermes_state — drop 81 re-exports + 3 registry aliases + 3 shims, repoint 45 callers + 60 test files
hermes_state.py: delete every '# noqa: F401 (re-exported...)' import block (hermes_state_common/errors/guard/
readpool/sessions/fts/dbfile/wal/repair/registry + agent.context_compressor _DB_PERSISTED_MARKER_KEY); keep
only the names hermes_state.py itself uses, without noqa.
hermes_state_registry.py: drop get_shared_session_db/release_shared_session_db/close_shared_session_dbs
aliases; every caller (gateway/, tools/, tui_gateway/, cron/, mcp_serve, run_agent, tests) now imports
acquire/release/close_all/release_or_close from hermes_state_registry.
hermes_state_titles.py: drop set_auto_title_if_empty shim (title_generator keeps its getattr fallback).
Re-remove shim-only names restored by 34abf954bd: latest_user_message_row_id (tests call
latest_message_row_id(key, role='user'); role-targeting assertions kept) and get_session_activity (tests
build the snapshot via agent.session_activity.build_activity_snapshot over db.get_session(sid)).
hermes_state_wal._log_once resolves its dedupe sets as module globals instead of via hermes_state;
hermes_state_repair helpers call module globals directly (tests patch hermes_state_repair.<name>).
Frozen updater surface untouched (update_cmd_maint imports only SessionDB from hermes_state).
2026-09-03 13:46:50 -07:00
Teknium ae2953ef01 refactor(agent/display,error_surface,i18n): final guard-ladder collapse 2026-09-02 19:32:53 -07:00
Teknium b2876bcba4 refactor(agent/display,error_surface): flatten preview/label/status-phrase branches, dict-build surface descriptors 2026-09-02 19:21:47 -07:00
Teknium 282688e37e refactor(agent/error_surface,i18n): fold layer resolution into helper, collapse guard ladders (strings untouched) 2026-09-02 18:34:22 -07:00
Teknium 6a88ec4eb3 refactor(agent/adapters): simplify azure identity, response guards, error surface, retry utils (-603 LOC)
Drop dead read_error_body_or_default / LAYER_RUNTIME; inline single-use
_build_default_credential/_safe_close; compact incident narratives to their
invariants in the guards. Byte-cap and deadline semantics of
read_streaming_error_body verified identical.
2026-09-02 13:29:47 -07:00
Teknium 334bcbac93 fix(desktop): error card honors the classifier's retry verdict + failing-session identity (review feedback)
Addresses @helix4u's review on #91493:
- conversation_loop now stamps failure_retryable (the real ClassifiedError
  verdict) next to failure_reason; error_surface prefers it and only falls
  back to the reason set for older results. Fallback set corrected to match
  classify_api_error (auth, format_error, billing_unverified now
  non-retryable).
- The descriptor carries the failing session's provider/model captured at
  classification time; Copy error details prefers them over the foreground
  composer atoms.
- Open logs is labeled 'Open Desktop logs' on remote/cloud connections —
  the local folder holds transport logs, not the remote runtime's.
- API-exception module allowlist widened to botocore/boto3/google/grpc/
  requests/aiohttp so other adapter SDKs don't misclassify as gateway.
2026-08-21 15:24:03 -07:00
Teknium 98f6fc549a feat(desktop): failed turns name the failing layer with recovery actions
Turn errors now carry a structured {layer, code, retryable} descriptor
(agent/error_surface.py) built from the same classifier the retry loop
uses. The tui_gateway stamps it on terminal error frames, retained
failed-turn snapshots, and resume replay; the Desktop error card renders
the layer title (provider / endpoint / streaming / auth / billing /
gateway / runtime / disk) plus matched actions: Retry, Switch provider,
Open logs, Copy diagnostics.

Older backends that omit the descriptor keep today's behavior (generic
title, string-sniff fallbacks) — the field is advisory on both sides.
2026-08-21 15:24:03 -07:00