Commit Graph

49670 Commits

Author SHA1 Message Date
kshitijk4poor
d0461ba6c3 refactor(desktop): free-tier challenge reuses the window-open policy and the contract's outcome type
- challenge-window.ts uses createWindowOpenHandler (deny + sanitised
  origin log, the link-title window's own hardening) instead of an
  inline deny.
- The renderer's outcome type is the generated contract's
  FreeTierChallengeResultParams['outcome']; global.d.ts uses the main
  process's ChallengeOutcome instead of a third hand-copied union, so a
  drift between the window and the contract fails the renderer typecheck.
- runFreeTierChallenge takes the gateway requester itself and builds the
  free_tier.challenge_result call, so lifecycle.ts and free-tier.ts stop
  each hand-rolling a reporter closure.

No behaviour change.
2026-10-06 23:14:10 +05:30
kshitijk4poor
d1f587331e fix(desktop): a required challenge ask never joins or inherits an optional run
The main process de-duplicated challenge windows by URL alone, and
remembered settled outcomes by url:attempt. If an optional (measuring,
never-revealed) run of a ticket was in flight, or had just ended
`failed` on #interactive, and the same ticket turned required, the
required ask got the optional run or its remembered `failed`, so no
window was ever shown for the human to clear. Key both the in-flight
run and the settled memory on URL + required.
2026-10-06 23:14:10 +05:30
kshitijk4poor
fa6ef8ba6d refactor(free-tier): one per-profile challenge record behind one lock
_pending (dict-of-dicts keyed "payload"/"deadline"/"outcome"),
_work_locks and _gave_up_until were three per-profile module globals,
two of them mutated outside the lock. They become one _ProfileChallenge
dataclass in a single _profiles map behind _state_lock; the record holds
the BrowserChallenge itself, so lookups are attribute reads and
as_payload() runs only when a status read asks for it. _set_pending
becomes _record(challenge, new_attempt=...). The test-only _presenter
seam goes: tests patch present() itself. _sleep stays, same idiom as
free_tier_bootstrap.

One edge differs, for the better: an EXPIRED same-URL record is now
treated as a new ticket (fresh deadline) instead of re-recorded with
min(new, old-expired) deadline, i.e. born expired.
2026-10-06 23:14:10 +05:30
kshitijk4poor
50b0082ed0 refactor(free-tier): build AnonChallengeRequired once; endpoint overrides table; codes live in anon_auth
- AnonChallengeRequired takes portal/credential/auth_state in its
  constructor. exchange_anon_jwt (given auth_state by its one caller,
  refresh_guest_state) builds it; the two catch-mutate-reraise blocks and
  the empty/None defaults are gone.
- _raise_for_anon_status no longer reads `portal_base_url=""` as an
  "is token exchange" flag. The exchange passes its verdicts as an
  `overrides` table keyed like _NAS_REFUSALS; 428 pow_* routes to an
  explicit proof-of-work verdict ahead of the 428 sign-in catch-all.
- ANON_CHALLENGE_REQUIRED / ANON_SIGNIN_REQUIRED and their copy are
  defined next to their siblings in anon_auth; anon_challenge imports
  them (and _anon_headers/_mint_memo_key) at module level, anon_auth
  imports anon_challenge lazily.
- client_info/user_agent read get_version_info().base_version instead
  of the old-updater __version__ shim.
- test: the stdio-TUI-gateway presentation test patches the real
  tui_gateway.server attributes; a SimpleNamespace stand-in broke the
  conftest server teardown.

No behaviour change.
2026-10-06 23:14:10 +05:30
kshitijk4poor
1eec4cf274 fix(free-tier): put the challenge link in front of ui-tui users
The TUI gateway child reports surface `cli`, so a 428 printed the link
to the child's stderr, which ui-tui keeps as a /logs tail and never
shows (createGatewayEventHandler: gateway.stderr). Over SSH or headless
that was a silent 120 s wait followed by "finish the check in your
browser" with no link. For a required challenge a foreground caller is
waiting on, the stdio TUI gateway now announces it on its JSON-RPC
channel, and ui-tui renders the message + link and opens one tab per
ticket. Background reads (keepalive, status paint) still never pop a
browser, and optional checks stay the desktop's to run hidden.
2026-10-06 23:14:10 +05:30
kshitijk4poor
4a370f1c76 fix(free-tier): only the desktop-spawned backend reports the desktop surface
client_surface() trusted raw HERMES_DESKTOP=1, which every terminal-pane
shell and agent child inherits (#116107 class). A `hermes` run in the
desktop terminal pane told NAS `surface: desktop` and suppressed its own
"finish the check in your browser" line. Use
process_identity.is_desktop_owned_backend(), which also needs the
per-spawn credential.
2026-10-06 23:14:10 +05:30
kshitijk4poor
e955022eb1 fix(proxy): never wait on a free-tier challenge under the Nous adapter lock
NousPortalAdapter._get_credential resolves credentials while holding
self._lock, which every proxied request queues on. With the challenge
gate on, a 428 parked that lock (and a default-executor worker per
queued request) for up to 120 s. Resolve as a background caller: the
challenge is recorded/announced and the retryable error returns at once.
2026-10-06 23:14:10 +05:30
kshitijk4poor
1507c8d0f7 fix(free-tier): a messaging gateway never waits on a browser challenge
The gateway resolves Nous credentials from async code with sync calls
(run_inbound._inbound_model_context_length -> runtime resolve, and
run_startup -> self_provision_relay -> resolve_nous_access_token). A 428
there parked the event loop for up to 120 s, and nobody sits at a
gateway's console to clear the check anyway. Treat the gateway surface
like a background caller: record/announce the challenge, raise the
retryable error at once.
2026-10-06 23:14:10 +05:30
kshitijk4poor
6dae8d059a fix(free-tier): keep the proof-of-work verdict for pow_* off a 428
The challenge commit dropped the `error == "pow_"` fallback in
_raise_for_anon_status. A pow_* error on a status with no wildcard row
(400/403/5xx) then became a retryable anon_server_error instead of the
terminal anon_pow_required, on every endpoint including sign-up and the
promotion paths the PR said were unchanged. Restore the fallback (kept on
purpose in 2a94ca80e7) and pin it on sign-up.
2026-10-06 23:14:10 +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
Austin Pickett
80cf5183f8 chore: map contributor emails for #133991
Add contributors/emails/ mappings so check-attribution resolves the
author emails on this branch:
- GuerrillaMedia702@gmail.com -> AP3X-Dev
- zaclanzon@users.noreply.github.com -> zaclanzon
2026-10-06 13:23:15 -04:00
Austin Pickett
fd431bedbc test(dashboard): per-call model attribution regression for #71778 2026-10-06 13:23:15 -04:00
Zac Lanzon
0cc2bcdcee fix(dashboard): count distinct models over the merged row set
totals.distinct_models was COUNT(DISTINCT model) over sessions alone, so a
model reached only through auxiliary usage drew a card the header never
counted. Derive it from the same merged rows the cards come from (#89631).
2026-10-06 13:23:15 -04:00
AP3X
2c48f5ff6c fix(dashboard): fold aux usage into the model card it belongs to
/api/analytics/models appended one row per (model, task, provider) from
_aux_usage_rows next to the sessions-derived (model, provider) row, so a
model doing approval/compression/title/background_review work rendered as
four or five identical-looking cards, and their session counts summed past
totals.total_sessions.

Fold aux usage into the existing pair's row; only an aux-only pair (no
sessions-derived row) becomes a new row carrying its own session count,
so dedicated aux models stay visible (#23270).

Ported from #89707 (web_server.py -> web_routers/analytics.py after the
router decomposition). Same diagnosis as #77524.

Co-authored-by: Kumar Sarthak <kumarsarthak0712@gmail.com>
Co-authored-by: Zac Lanzon <zaclanzon@users.noreply.github.com>
2026-10-06 13:23:15 -04:00
Austin Pickett
d40794140b fix(dashboard): attribute models usage per API call from session_model_usage 2026-10-06 13:23:15 -04:00
unsupportedpastels
aac7ad880d fix(scratch): escape control characters in prune-log paths and names
Review of #133952 (ehz0ah) showed a legal POSIX name containing a newline was written across two lines of scratch-prune.log, so one record split and the tail of the name could pass for a record of its own. Paths and process names are now logged with %r, so control characters are written escaped and each record stays one line.

Refs #132401.
2026-10-06 13:22:50 -04:00
unsupportedpastels
788976407a fix(scratch): keep prune-log records for non-UTF-8 names and concurrent rollover
Review of #133952 (andrexibiza) reproduced two ways a deletion could leave no line in scratch-prune.log: a legal POSIX name that is not UTF-8 hit the strict utf-8 handler and the whole record was dropped, and when two prunes of one home rolled the log over at once, the second rename found the file gone and the record was dropped with the FileNotFoundError.

The handler now writes with errors=backslashreplace, and its rollover treats a source renamed away by a concurrent prune as already rotated and writes the record to the new file.

Refs #132401.
2026-10-06 13:22:50 -04:00
unsupportedpastels
67784d8a89 fix(scratch): record every pruned entry and reaped process in logs/scratch-prune.log
The idle prune deleted entries and TERM/KILLed processes with one aggregate INFO line, invisible at the default WARNING level and absent entirely at boot, before setup_logging() runs. It also counted an entry as removed even when rmtree (ignore_errors) left it behind.

Each removed entry (path, size), each signal sent (pid, name, cwd) and each entry that could not be removed now gets a line in <home>/logs/scratch-prune.log, through a RotatingFileHandler of its own (fixed 5 MB x 3, as tool_calls.log), so the boot-time prune is recorded too. The handler belongs to one prune and is closed when it ends: no process holds the file open between prunes (an open handle blocks rollover on Windows), and prunes of two homes never share it. Only entries that are really gone are counted. A process that exited but is not yet collected (zombie) is not sent KILL.

Refs #132401. Salvages #132455.

Co-authored-by: Justin Sharpe <2106314+justinsharpe@users.noreply.github.com>
2026-10-06 13:22:50 -04:00
Mark S.
970ba79fb4 Merge pull request #133975 from victor-kyriazakos/fix/own-patch-carries-write-baseline
fix(tools): a task's own patch keeps its full-file write baseline
2026-10-06 13:14:27 -04:00
Brooklyn Nicholson
cd94de7ea4 test(desktop): tool disclosure cache evicts on unmount
Regression test for #131121: after subscribe -> unsubscribe (a row's
mount -> unmount), a second call with the same id must return a fresh
atom — the entry was released instead of being retained for the
window's lifetime — while identity stays stable while subscribed.

Co-authored-by: Yuan Li <dskwelmcy@163.com>
2026-10-06 12:09:55 -05:00
Yuan Li
b8c844b99a fix(desktop): evict inactive tool disclosure atoms 2026-10-06 12:09:55 -05:00
Kuxin
0fa1fb600d fix(stt): normalize local command language hints 2026-10-06 12:08:04 -05:00
Kuxin
0d234e714c fix(stt): normalize local language hints 2026-10-06 12:08:04 -05:00
Austin Pickett
35f0b4b569 fix(dashboard): route native text paste through term.paste to stop last-char duplication
A native/DOM paste on the chat terminal (context-menu paste, middle-click,
or any Ctrl+V route that bypasses the keydown interception) fell through to
xterm's own paste listeners. xterm clears the hidden textarea during the
paste dispatch, but the browser's default action — inserting the pasted
text into that textarea — runs after dispatch, so a stale value survives
and the next typed character is duplicated (#52471: "when" → "whenn").

The host capture-phase paste handler now cancels the native paste for
plain text and delivers it via term.paste() exactly once — the same clean
route the Ctrl/Cmd+V keydown interception already uses. Image pastes keep
their upload path and are never routed as text.

Supersedes #52477 (credit: @liuhao1024 — same interception, extended with
the image route preserved and regression coverage).

Fixes #52471
2026-10-06 13:00:43 -04:00
Austin Pickett
7ed284fee0 fix(web): satisfy code-health ratchet and contributor attribution
- PtySession.close and _discard_active_session_file: waive the deliberate
  teardown catch-alls with reviewed '# health: allow' comments (BLE001+S110).
- pty_ws was over its main-pinned CC cap: extract the channel-marker prologue
  into _pty_channel_marker_state and the keep-alive writer loop into
  _pump_keepalive_input.
- Map sinh3543@gmail.com -> sinh2024 (check-attribution, PR #133936).
2026-10-06 12:46:06 -04:00
Austin Pickett
3178ef3e21 test(pty): the channel marker dict is empty after every PTY lifecycle end
Regression coverage for #63553: _active_session_file_for_channel
registered channel→tempfile entries that no code path popped, so
pty_active_session_files grew without bound and churned a forgotten
tempfile+inode per reconnect.

Proves the invariant across every session end/forget path:
- keep-alive close (close_all, same path as TTL reap / child EOF)
- repeated same-channel reconnects keep one mapping
- legacy 1:1 pump (no attach token) pops its marker on disconnect
- ?fresh=1 drops the stale breadcrumb yet keeps the mapping so the
  next same-channel reconnect resumes the fresh child (the regression
  flagged in the #63563 review)

All four are red on 85db7c3a68.
2026-10-06 12:46:06 -04:00
sinh2024
56227dd6fd fix(web): clean up PTY active session markers 2026-10-06 12:46:06 -04:00
Austin Pickett
ee2ed10ef5 fix(sessions): validate imported values before persistence
Session import used shallow type casts at the persistence boundary:
float() accepted inf/NaN (a JSON 1e309), int() accepted integers
SQLite cannot bind (outside signed 64-bit range), model_config was
only shape-checked after its intentional decode, and a literal string
carrying the internal \\x00json: content marker was decoded as
structured content on read. Bad values reached sqlite3 binding or
persisted as inf, breaking session APIs with HTTP 500.

- reject non-finite floats in session float/timestamp columns, message
  timestamps, and scalar message content, with the field path in the error
- re-check every intentionally-decoded JSON-backed field (model_config,
  reasoning_details, codex_* items) for non-finite values
- bound int columns, token_count, and int scalar content to SQLite's
  signed 64-bit range; coerce-with-default at the bind site as a backstop
- escape literal \\x00json:-prefixed strings via the JSON wrapper instead
  of storing them raw; decoding already fails closed on malformed marker rows
- all rejection stays inside the existing atomic validate-then-write
  400 contract; valid exports round-trip unchanged

Fixes #63954
2026-10-06 12:40:04 -04:00
Austin Pickett
3225f357a5 fix(dashboard): show broken links as non-actionable entries in Files page
Backend listings now emit broken_link: true placeholder entries with
nullable mtime for dangling symlinks. Update ManagedFileEntry to match,
render broken links with an Unlink icon and a destructive badge, disable
download for them, and keep delete enabled so users can remove the dead
link from the UI.
2026-10-06 12:30:01 -04:00
Austin Pickett
1ca192bc1b fix(dashboard): don't 500 file listings on dangling symlinks or vanished entries
The dashboard file browser's listing route built entries via a bare
comprehension over os.scandir, calling _managed_file_entry() per entry.
That helper raises HTTP 500 ("Could not stat path") on any OSError from
resolved.stat(), so a single dangling symlink (or an entry removed
between readdir and stat) aborted the whole directory listing with a 500
— e.g. a broken ~/.claude link or Steam's recreated linux32/steam made
the entire Files page unusable (#47154).

Whole-class fix, consolidated from the open contributor PRs:

- _managed_file_entry() classifies a symlink whose target is definitively
  missing (FileNotFoundError) before the resolved-target boundary check
  and returns a `broken_link: true` placeholder entry instead of 500ing.
  The placeholder is only emitted for entries inside the managed root
  even when the missing target resolves outside it. Permission and other
  OSErrors still surface as 403/500 as before.
- Symlink stat results are captured in one snapshot (no second stat), so
  a target disappearing mid-listing cannot flip a result into a 500.
- New skip_missing flag lets the listing scan skip an entry that
  vanished between readdir and stat (returns None); the write-result
  caller stays strict.
- The listing loop appends placeholders and skips vanished entries, so
  one dead entry never loses its siblings. The sibling fs_list route
  already tolerated ENOENT; /api/files now agrees.

Reading/downloading a broken link directly still returns 404; 403
boundary and sensitive-path behavior are unchanged.

Fixes #47154

Co-authored-by: gangpangpang <2564186295@qq.com>
Co-authored-by: ous fifty <ousfifty@gmail.com>

Incorporates work by @Lqg97 (PR 47159; co-contributors qinggangli and Shiqian Ma, commit emails not linked to GitHub accounts).
2026-10-06 12:30:01 -04:00
Austin Pickett
ca38caeea0 fix(dashboard+auth): cap HTTP JSON response body reads
Dashboard-side urllib readers (gateway health probe, ElevenLabs voices)
and self-hosted OIDC httpx readers (discovery, token exchange, revocation)
buffered remote response bodies with no size bound — a hostile or
compromised upstream could exhaust memory. Both halves now read with a
1 MiB hard cap: a shared _read_dashboard_json_response for urllib JSON
reads and a streaming _request_limited_response for IdP endpoints that
prechecks Content-Length and caps the chunked body regardless.

Adapted from #54809 and #55122 (credit @ooiuuii) to current main, where
the token exchange lives in plugins/dashboard_auth/_shared.py and the
health probe in hermes_cli/web_server_gateway.py.

Fixes #54808, Fixes #55121
2026-10-06 12:28:55 -04:00
Austin Pickett
e93446126d fix(dashboard): surface the messaging platform's auth posture in the Test result
The Channels 'Test' button stopped at connectivity: a platform whose gateway
authorization posture denies every sender (no allowlists, no allow-all flag, no
pairing grants) still answered '<Platform> is connected.' — a green checkmark
on a bot that ignores everyone (#66030).

When the adapter reports connected, the test result now carries the platform's
access posture for the ACTIVE profile, mirroring the decision ladder of
AuthorizationMixin._principal_authorized:

- open: {PLATFORM}_ALLOW_ALL_USERS / GATEWAY_ALLOW_ALL_USERS
- allowlist: {PLATFORM}_ALLOWED_USERS, GATEWAY_ALLOWED_USERS, the Telegram
  group allowlist vars, or a config-enforced dm_policy: allowlist
- pairing: approved pairing grants in the profile's pairing store
- deny_all: nothing configured — with pending pairing requests counted in
  the message

The check is profile-aware: under a named profile it reads that profile's
.env/config/pairing store, never the dashboard process's os.environ (which
carries the ROOT install's credentials). ok stays True (the connection is up);
the new auth_posture field lets the UI render yellow instead of green.

Reuses the idea from PR #66055 (authored by @webtecnica) with the evaluator
made profile-aware and extended to cover GATEWAY_ALLOWED_USERS, pairing grants
and adapter/config-enforced allowlists, per its review.

Fixes #66030
2026-10-06 12:28:06 -04:00
Austin Pickett
82f2dcb286 fix(gateway): let GET /api/model/options opt out of unconfigured skeleton rows
include_unconfigured was hardcoded True in _handle_model_options, so external
API consumers had no way to request the explicit-only view. Forward the
include_unconfigured query param (default true, back-compatible) to
build_model_options_payload. With _append_unconfigured_rows now honoring
model_catalog.excluded_providers, this endpoint no longer resurrects excluded
providers on any view (#68816).

Also lifts model_options decoding (_request_reasoning_config /
_request_service_tier) into gateway/platforms/api_server_request_options.py to
offset api_server.py's frozen line cap (moved code keeps its cap; the facade
re-exports both names).
2026-10-06 12:28:00 -04:00
PRATHAMESH75
93ca74c3bb fix(cli): honor excluded_providers in the TUI /model picker's unconfigured rows
build_models_payload(include_unconfigured=True) — the path the in-session TUI
/model command uses — appended CANONICAL_PROVIDERS skeleton rows via
_append_unconfigured_rows() without consulting model_catalog.excluded_providers.
So every excluded provider reappeared in the TUI picker even though hermes model
(list_authenticated_providers) hid it correctly.

Filter excluded providers in the skeleton loop, normalized the same way
list_authenticated_providers does ({str(p).strip().lower()}), and exclude
unconditionally — including the configured-current provider — to match that
path, which drops an excluded provider even when it is current.

Adds include_unconfigured-path coverage: excluded provider removed (others
intact), case-insensitive match, current-excluded-provider dropped, and
empty/None excluded is a no-op.

Fixes #68816
2026-10-06 12:28:00 -04:00
Austin Pickett
da303d1729 fix(dashboard): preserve CLI-configured custom endpoints on same-endpoint model/set
POST /api/model/set re-registered a named custom_providers row for any
custom/local main assignment, even when the endpoint was already a fact
in config.yaml (the CLI gateway setup writes provider: custom with an
inline base_url + api_mode). The picker then rewrote the CLI's shape into
custom:<slug> with base_url: '' (#76324).

Snapshot the pre-save config and skip registration when the endpoint is
already known (inline model.base_url, a providers: entry, or a
custom_providers row); only a genuinely new endpoint gets a named row.

Fixes #76324
2026-10-06 12:27:41 -04:00
Austin Pickett
5515c4be55 fix(dashboard): backfill /api/sessions/search with title matches
The search endpoint composed only search_sessions_by_id (id/lineage LIKE)
with search_messages (FTS5 over message content), so a term that lived
solely in a manually-set sessions.title returned zero results — renamed
sessions were invisible to search (#66242).

The DB layer already knew how to LIKE-match titles across the whole
compression chain: list_sessions_rich(search_query=...) via
_chain_search_where (the same helper the sidebar listing uses). Reuse it
rather than adding a second title query path: after the id and FTS lanes,
backfill remaining result slots through list_sessions_rich, feeding rows
through the same add_lineage_result dedup path so lineage, limits, and
result shape stay one contract (content hits keep BM25 priority; the
title lane is best-effort on top).

Fixes #66242
2026-10-06 12:27:29 -04:00
Austin Pickett
fbf261a33f fix(web): preserve repeat count when creating cron jobs from dashboard
CronJobCreate did not declare a repeat field, so Pydantic dropped it during
request parsing, and _create_cron_job_sync never forwarded one to
cron.jobs.create_job — a finite repeat count submitted via POST /api/cron/jobs
was silently stored as an unlimited recurring job.

- Add optional repeat field to CronJobCreate (int or CLI string form)
- Forward it from _create_cron_job_sync; non-positive counts 400, string
  forms coerce through the shared normalize_repeat_value the CLI uses
- Add repeat?: number to the desktop CronJobCreatePayload type
- Route-level regression coverage: finite survives create+list, omission
  stays unlimited, invalid counts 4xx with no job stored
2026-10-06 12:27:22 -04:00
Austin Pickett
0620913eac fix(dashboard): gate the chat WebGL renderer and fall back to DOM
The chat terminal loaded @xterm/addon-webgl for every wide host with
nothing but a constructor try/catch. Three failure classes escaped it:

- Safari's WebGL atlas garbles Unicode box-drawing glyphs (#18773)
- software rasterizers (llvmpipe/SwiftShader) advertise WebGL but regl
  throws '(regl) webgl not supported' from inside the addon (#45520)
- the per-glyph atlas cannot shape complex scripts, so Bengali
  conjuncts render broken (#58685)

Gate the addon behind a pre-flight probe (real context + non-software
unmasked renderer, released immediately) and a Safari UA check, both in
a new pure module; when shaped-script text arrives over the PTY, drop
the live addon so xterm re-renders through the DOM renderer, whose text
shaping handles conjuncts. Context loss already disposed the addon.
2026-10-06 12:26:53 -04:00
Austin Pickett
e30c6aecef fix(desktop): a session-picker click re-selects the active session
The picker door (openSessionFromPicker — /resume overlay, ⌘K, artifacts)
only ran openSession, whose in-place path treats the already-active
session as a front-only no-op. The sidebar's own door always queues an
explicit requestSessionResume, which use-route-resume honours via its
explicitlyRequested branch to re-run resumeSession even when the session
is already active. Give the picker the same explicit reselect so clicking
the active session re-attaches the transcript and re-focuses the composer
(#62045) instead of doing nothing.
2026-10-06 12:24:07 -04:00
JoaoMarcos44
70b8d815e6 test(desktop): pin caret rekey semantics 2026-10-06 11:19:00 -05:00
JoaoMarcos44
5beac8a5ae fix(desktop): preserve caret on same-draft scope rekey 2026-10-06 11:19:00 -05:00
Brooklyn Nicholson
02a802a7ef fix(desktop): stream Windows update-marker probe over stdin
The marker probe (~8.3k chars encoded) was the second Windows SSH
command still over cmd.exe's 8191-char limit after the platform probe
moved to stdin: it died with 'The command line is too long' and the
relaunch gate could never observe CLEAR. Ride the same stdin transport
(powerShellStdinCommand) instead of splitting the probe, keeping the
C# no-follow marker reader and its fail-closed semantics intact, and
assert every emitted Windows probe command stays under the limit.

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

Fixes #106716
Refs #106903
2026-10-06 11:15:52 -05:00
thegreatesthoneybee
25d7de6fad fix(desktop): preserve Windows SSH probe input encoding 2026-10-06 11:15:52 -05:00
thegreatesthoneybee
5dd73d4641 fix(desktop): stream Windows SSH probe over stdin
Avoid the Windows cmd.exe command-line limit by sending the PowerShell platform probe over SSH stdin instead of embedding a long encoded command. Keep probe statements newline-separated so PowerShell try/catch blocks parse correctly, and cover the real PowerShell path with regression tests.
2026-10-06 11:15:52 -05:00
Austin Pickett
aaf7f199d5 fix(state): chain-aware dashboard session deletes; stats exclude compression children
Dashboard sessions endpoints treated compression-continuation children as
independent rows in two places:

- /api/sessions/stats counted every physical row, so total/active_store/
  archived were inflated by the hidden chain links while the paired
  list_sessions_rich page collapses each chain to one row (#54298).
- The bulk and single delete endpoints removed only the selected physical
  row. A list row carries the chain tip's id, so the root survived and the
  next list reload re-projected it onto the previous link — deleted
  conversations reappeared (#57543).

delete_session/delete_sessions gain include_compression_chain (opt-in; the
dashboard endpoints pass True). Lineage expansion walks compression edges in
both directions with the same edge definition as get_compression_tip and the
list-projection CTE (branch/reset/delegate/tool children excluded), collects
stale sibling continuations, threads the write-guard pass through the
expanded lineage, sweeps transcript files of every chain member, and keeps
the returned count equal to the selected rows. The default stays chain-blind
for existing callers (CLI /exit --delete, ACP, gateway API).

Fixes #54298, Fixes #57543

Salvaged from #54300 (stats exclude_children; @liuhao1024) and #57996
(lineage expansion; @draix), adapted onto the web_routers split and the
guard-aware delete paths.

Co-authored-by: liuhao1024 <sunsky.lau@gmail.com>
Co-authored-by: draix <draixagent@gmail.com>
2026-10-06 12:03:11 -04:00
Austin Pickett
535f2161dc test(skills): pin ambiguous display-name lookup — refuse to guess
Two skills sharing a frontmatter display name must not resolve by that
name in skill_manage (silently mutating the wrong skill); a unique
display name resolves and the directory names keep working. Mirrors
skill_view's same-tier collision refusal semantics in the manager.
2026-10-06 12:00:47 -04:00
Austin Pickett
cff9f5eb1d fix: resolve dashboard skills by display name
Originally authored by @deanjo in https://github.com/NousResearch/hermes-agent/pull/80883 (commit email not linked to a GitHub account).
2026-10-06 12:00:47 -04:00
liuhao1024
19b5b411ab fix(skills): resolve the frontmatter name _find_skill ignores
skills_list displays a skill's frontmatter name: when it diverges from
the directory name, every mutating skill_manage action (and the
/skills approve replay) called with that displayed name failed with a
misleading 'not found in active profile' error — nothing about profiles
was actually wrong (#99609).

_find_skill now falls back to matching the SKILL.md frontmatter name
after the directory-name/categorized scan finds nothing, with the same
MAX_NAME_LENGTH truncation skills_list applies when displaying it.
Directory-name matches keep priority (recorded only, returned after the
full scan), so one skill's frontmatter cannot shadow another skill's
directory. This also makes _create_skill's duplicate check catch a new
directory name colliding with an existing skill's displayed name — a
collision skills_list's seen-names dedup would silently hide.
2026-10-06 12:00:47 -04:00
Austin Pickett
aa30e02d86 fix(dashboard): badge follows the PTY runtime model, not config, during fallback (#54509) 2026-10-06 11:44:38 -04:00
Austin Pickett
dca25fe74d refactor(web): extract terminal appearance helpers; satisfy code-health ratchet
ChatPage.tsx crossed its FILE_LINES cap and the ChatPage function its
FUNC_LINES cap once the #36784 accessibility lines landed. Move the
module-level theme/font-tier helpers to web/src/lib/terminal-appearance.ts
(the ratchet's sanctioned remedy — moved code keeps its cap) and tighten
the new comments. No behaviour change: same helpers, same values.
2026-10-06 11:44:32 -04:00