Commit Graph

49635 Commits

Author SHA1 Message Date
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
Austin Pickett
fe6eb7f976 test(web): assert composer screen-reader mode behaviourally (#36784)
Replace the static source-text assertions with behavioural tests that
render ChatPage against a capturing FakeTerminal and observe what
assistive technology observes: the Terminal is constructed with
screenReaderMode: true, the host div is a labelled role=region, and the
single registered key handler lets Tab escape (WCAG 2.1.2) while other
keys still reach the terminal. Proven RED on base, GREEN with the fix.
2026-10-06 11:44:32 -04:00
David Metcalfe
a29eb0d8e0 fix(web): fold Tab escape into existing xterm key handler (#59091 review)
Teknium's review of the original PR flagged that the new
`term.attachCustomKeyEventHandler` at line 540 silently overwrote
the clipboard handler at line 464 — xterm.js stores a single
`_customKeyEventHandler` per Terminal
(node_modules/@xterm/xterm/src/browser/CoreBrowserTerminal.ts:914,
`this._customKeyEventHandler = customKeyEventHandler;`) and assigns
overwrites later registrations, so the Tab escape branch never
executed.

The rework folds the Tab branch into the existing handler at the
top (right after the type guard) so only one handler is registered.
The ARIA region and screenReaderMode additions remain untouched.

Adds web/src/pages/ChatPage.a11y.test.ts — regression test asserting
that `screenReaderMode: true` is set, the host div carries
`role="region"` + `aria-label=`, exactly one
`term.attachCustomKeyEventHandler(` call exists, and the single
handler contains the Tab-escape branch + keydown guard. Verified the
test fails on the original PR diff (catches the regression) and
passes on the rework.

Closes inline review thread at #59091.
2026-10-06 11:44:32 -04:00
David Metcalfe
cc26049c52 fix(web): enable screen-reader mode for chat composer (#36784)
The web UI chat composer uses xterm.js, which renders to a canvas
invisible to VoiceOver/NVDA. Add screenReaderMode: true to create
the offscreen textarea screen readers need, a Tab escape handler to
prevent keyboard traps (WCAG 2.1.2), and an ARIA region label on
the host div.
2026-10-06 11:44:32 -04:00
unsupportedpastels
0dbaf33f67 fix(gateway): measure fallback drift from the session's winning model on its primary route
The channel-override baseline from #131295 skipped the session /model tier and
normalized the expected model with the agent's current provider. Both disagree with
how the agent was built, which turned three cases main handled into wrong verdicts:

- a recorded Nous switch off a session /model choice was evicted (the baseline was
  the channel model, so the (alias, backing) record never matched);
- a session-primary fallback that landed on the channel's model was kept;
- an OpenRouter primary that fell back to native Anthropic was kept, because the
  channel slug normalized under the fallback provider equals the fallback model.

Resolve the baseline in the same order as _resolve_session_agent_runtime (session
/model, channel_overrides, global) and normalize it for the agent's primary provider,
the same route the welcome-host pin already uses. A session override whose provider
was unavailable this turn ran on the default route (the resolver drops it for that
turn and retries next turn), so it is the baseline only when its provider is the
agent's primary provider.

A model-name comparison cannot see a fallback that serves the configured model on
another endpoint; with a channel model that differs from the global one, #131295
turned that case from evicted into kept. A turn whose agent has
_provider_fallback_active (set only by _try_activate_fallback, cleared by
restore_primary_runtime) now evicts before any name comparison.

The baseline lives in GatewayAgentCacheMixin._fallback_baseline_model beside
_is_intentional_model_switch, which keeps gateway/run_turn.py under its code-health
line cap (the channel lookup from #131295 had grown it past main).

Session tier taken from #132238; both regressions diagnosed in review by @andrexibiza.

Co-authored-by: salch-cred <141555468+salch-cred@users.noreply.github.com>
Co-authored-by: andrexibiza <84248988+andrexibiza@users.noreply.github.com>
2026-10-06 11:24:29 -04:00
teknium1
e72180bc7a test(gateway): channel_overrides turns keep one cached agent through the real turn path
Review fix-up for #131295. The committed cases call _run_agent_evict_on_fallback
directly with a SimpleNamespace and a mocked _evict_cached_agent, so they prove the
classifier's verdict but not the cache/pin lifecycle the incident is about. This drives
the real handler -> _run_agent -> TurnRunner.run_sync path (only the AIAgent class and
config reads are substituted) through human -> internal wake -> human and asserts one
agent instance, the override model in _agent_cache, and a surviving ephemeral pin.

Red/green across the branch's history:
  origin/main       -> named-channel-model fails (the #131294 eviction)
  7a3e9f5 (1st fix) -> welcome-host-pins-over-channel-model fails (the R1 regression)
  cdf5e12 (head)    -> both pass

Co-authored-by: Guerilla-ops <307102608+Guerilla-ops@users.noreply.github.com>
(cherry picked from commit 46e9f20e8c562dfcddf1c11b4d08874d5cd842a2)
2026-10-06 11:24:29 -04:00
EloquentBrush0x
b23877a5ec fix(gateway): the welcome host's pinned model is not a fallback either
The channel_overrides comparison in _run_agent_evict_on_fallback took the
channel's model as the expected one, but AIAgent.__init__ pins an agent on
the Nous welcome host to nous/welcome (pin_model_for_route) whatever the
chat configured. With a global nous/welcome and a channel override, that
looked like a fallback and evicted the cached agent after every turn
(reported in review by andrexibiza).

Apply the same pin to the expected model, on the agent's primary route
(_primary_runtime) so a fallback that lands on the welcome host still
evicts. The recorded alias->backing switch keeps matching
_is_intentional_model_switch.

(cherry picked from commit cdf5e12666a501eb02940ccab90ea05ce65d0b96)
2026-10-06 11:24:29 -04:00
EloquentBrush0x
1df1f13417 fix(gateway): a channel_overrides model no longer evicts the cached agent after every turn
_run_agent_evict_on_fallback compared the agent's model with the global config model, so a
chat, thread or parent with channel_overrides.<id>.model looked like a fallback after every
successful turn: the cached agent was evicted, rebuilt next turn, and its context pin cleared.
d6c53dcdcb fixed two other causes of the same per-turn eviction.

Compare with the channel's model, resolved through _resolve_model_for_channel with the
thread/parent lookup _resolve_session_agent_runtime uses. Without an override it is the
global model, as before. Tests cover a chat and a thread of an overridden parent, and a
fallback off the channel model still evicts.

(cherry picked from commit 7a3e9f5754bbb2597267ce499eac2658d7c9f17a)
2026-10-06 11:24:29 -04:00
Austin Pickett
ccff4df98c fix(skills): trust-scope inline-shell auto-exec to non-community hub skills
Inline `!`cmd`` expansion in SKILL.md bodies ran for every skill whenever the
global skills.inline_shell flag was on — a community hub skill and a builtin
were expanded identically. The hub's install gate (INSTALL_POLICY) blocks a
community install on the caution/dangerous verdicts the inline-shell DSL scans
as, but --force or a pre-scanner install puts that skill on disk anyway, and
the next view/slash/preload would auto-execute it on the host.

Gate expansion at the single preprocess_skill_content choke point (all three
surfaces route through it): community-trust hub lock entries never expand;
trusted/builtin hub entries and anything without a hub lock entry (bundled-
synced, user-created, project/external) keep the flag's contract. Lock read
failures skip the gate, like every other provenance consumer.

Fixes #63307
2026-10-06 11:20:43 -04:00
Austin Pickett
733d16973e fix: keep fs_read_text callable without a Request
The confinement guard reads only env/home state, so the Request the PR
threaded through fs_read_text/fs_download is unused. Make it optional
(request: Request = None keeps FastAPI injection while direct test
callers stay source-compatible) and drop the mandatory argument from
_hosted_fs_read_guard/_managed_files_policy.
2026-10-06 11:19:16 -04:00
Adolanium
b9d9aeeb58 fix(dashboard): confine fs preview and git diff reads to the managed root
The managed-files API (#57505) confines locked-dashboard reads through
_resolve_managed_path, but the file-preview and git-review routes resolve
through unconfined _fs_path / _git_path. On a locked deployment those
routes still returned auth.json / .env / mcp-tokens and any path outside
the managed root (including via git diff --no-index on an absolute file
argument).

Apply the same locked-root and sensitive-path checks to:
- /api/fs/list, /api/fs/read-text, /api/fs/read-data-url
- /api/git/review/diff and /api/git/file-diff (resolved file target)
- /api/git/review/commit-context (caller-supplied repo cwd; this route
  returns git diff --cached / git diff HEAD with no per-file arg, so the
  cwd itself is the read target)

Local dashboards with no locked root are unchanged. Regression tests cover
credential and outside-root blocks plus in-root allow paths, including a
locked-mode outside-root case for commit-context.

(cherry picked from commit b6ed59823f22fc537f123407a01915135dd6d5fe)
2026-10-06 11:19:16 -04:00
Austin Pickett
debc183d45 test(dashboard): password-form script test needs a password provider registered 2026-10-06 11:17:11 -04:00
Austin Pickett
082fae8058 fix(dashboard): cover whole prefix class — font URLs, native chooser, logout landing
Every root-absolute URL the server-rendered auth pages emit now honours
X-Forwarded-Prefix: @font-face url('/fonts/...') in both the login page
and the RFC 8252 native provider chooser, the chooser's data-prefix
injection, and the SPA logout navigation target. Empty-prefix output is
byte-identical to before.
2026-10-06 11:17:11 -04:00
liuhao1024
e33cf1b256 fix(dashboard): prefix post-login landing behind reverse proxy
When the dashboard is mounted at a sub-path via X-Forwarded-Prefix,
auth_callback and auth_password_login returned bare "/" as the
post-login landing URL instead of prepending the prefix. This caused
the browser to navigate outside the proxy mount after a successful
login.

Add _externalize_post_login_target() helper that prepends the prefix
to the validated landing path, and apply it in both auth_callback and
auth_password_login. Paths that already carry the prefix are returned
unchanged; empty prefix (direct deploy) is a no-op.

Reported-by: LeDominik
2026-10-06 11:17:11 -04:00
liuhao1024
53e5965b24 fix(dashboard): prepend X-Forwarded-Prefix in _safe_next_target() for post-login redirect
When the dashboard is behind a reverse proxy at a sub-path (e.g. /hermes),
the post-login redirect next= parameter must include the prefix so the
browser lands at /hermes/sessions instead of bare /sessions.

Auth-route and API-path rejection still operates on the original path
(before prefix) so detection works regardless of the mount point.

Addresses feedback from @grunjol and @LeDominik on #48625.
2026-10-06 11:17:11 -04:00
liuhao1024
aff64c886a fix(dashboard): include BASE path in logout redirect to /login
The logout handler used a hardcoded `/login` path for the post-logout
redirect. When the dashboard is served under a URL prefix (via
X-Forwarded-Prefix), this navigates to the wrong path, breaking the
logout flow for reverse-proxied deployments.

Use `${BASE}/login` to match the prefix-aware pattern already used
by the API calls in the same file.
2026-10-06 11:17:11 -04:00
liuhao1024
324fb9fdd0 fix(dashboard): respect X-Forwarded-Prefix in server-rendered login page
The login page emitted root-relative URLs (/auth/login, /auth/password-login)
for provider buttons and the password-login JS, breaking the OAuth and
password flows when the dashboard is served behind a reverse proxy at a
sub-path (e.g. /hermes/).

Now render_login_html() accepts a prefix parameter (resolved from the
X-Forwarded-Prefix header via the existing prefix_from_request() helper)
and prepends it to all auth URLs. The password-login JS reads the prefix
from a data-prefix attribute on the <main> element.

Fixes #48623
2026-10-06 11:17:11 -04:00
Austin Pickett
db26e55742 fix(dashboard): serve SPA assets with explicit MIME types
On Windows, mimetypes.init() reads the registry, where .js/.mjs can be
mapped to text/plain; starlette StaticFiles/FileResponse then serve the
dashboard bundle as text/plain, browsers reject the ESM module script
under strict MIME checking and the SPA stays blank (#28987).

Pin the served extensions (.js/.mjs/.cjs/.css/.json/.map/.svg/.wasm/
.webp/.webmanifest/.woff/.woff2/.ico) with explicit mimetypes.add_type
calls at web_server_dashboard import, so Content-Type is
platform-independent regardless of the host map. Idempotent.

Reuses the approach of closed PRs #28996 (xxxigm) and #29254
(Jiahui-Gu).
2026-10-06 11:15:27 -04:00
liuhao1024
727dbb17f2 fix(dashboard): move CORSMiddleware outermost so OPTIONS preflight bypasses auth
app.add_middleware(CORSMiddleware, ...) was registered before the
@app.middleware(http) auth decorators, making CORS the innermost
layer in Starlette's onion model.  A cross-origin OPTIONS preflight
hit auth first → 401, and the CORS headers were never sent.

Moving the add_middleware call after all @app.middleware registrations
makes CORS outermost, so preflights are answered before auth runs.

Fixes #59052
2026-10-06 11:15:20 -04:00
luyifan
d59c736fcb Cap dashboard password auth rate buckets 2026-10-06 11:14:42 -04:00
Austin Pickett
ba2fd63ef2 test(dashboard-auth): plugin assets pass the OAuth gate; /api/* still gated
Co-authored-by: Dave Bunn <davebunn1@gmail.com>
2026-10-06 11:14:34 -04:00
Sahil-SS9
d04d421944 fix: exempt /dashboard-plugins/ from OAuth auth gate (#53066) 2026-10-06 11:14:34 -04:00
Austin Pickett
c5cb0f597b fix(dashboard-auth): clock-skew leeway for JWT verification; classify not-yet-valid tokens as validation failures
Both dashboard-auth JWT verification paths (self-hosted OIDC ID token and the
Nous Portal access token, via the shared verify_jwt) called PyJWT jwt.decode
with the default leeway=0 while requiring iat. Any host whose clock lags the
issuer's — unsynced containers/VMs, NTP jitter — failed login with
ImmatureSignatureError, and the failure was folded into ProviderError, i.e.
HTTP 503 'provider unreachable', discarding already-rotated refresh tokens
and forcing unrecoverable re-logins.

- verify_jwt gains leeway (default 60s, RFC 7519 §4.1.4-4.1.6) and classifies
  ImmatureSignatureError as InvalidCodeError (validation failure -> 401 /
  refresh) instead of ProviderError/503.
- New parse_leeway helper: fail-closed parsing (unparseable / negative / NaN /
  inf -> 0, never widens the window); 0 honours strict mode.
- Self-hosted OIDC: id_token_leeway kwarg + dashboard.oauth.self_hosted.id_token_leeway.
- Nous: token_leeway kwarg + dashboard.oauth.token_leeway.
- No new env vars (config.yaml-only knobs).

Fixes #47815
2026-10-06 11:14:28 -04:00
Austin Pickett
bc1f26792b fix(dashboard-auth): rotate dashboard-auth.log at 5MB/3 backups
audit_log() appended with a plain open(path, 'a') and no size cap, so
dashboard-auth.log grew unbounded for the life of the install — the one
log in the auth path outside hermes_logging's rotation.

Add self-contained size-check-and-rename rotation in audit.py matching
the agent.log convention (maxBytes=5MB, backupCount=3, stdlib
.1/.2/.3 naming). The pending record's UTF-8 bytes count toward the
threshold, so a crossing write rotates before appending. Implemented
locally (stdlib os.replace only) to keep the module import-light for
early-loading middleware; reusing _ManagedRotatingFileHandler would
pull in hermes_constants and break that guarantee.

Fixes #57749
2026-10-06 11:10:45 -04:00
ethernet
baef00bf4c fix(smoke): check bundled Hermes home before sandbox setup
The smoke driver created .desktop-smoke-home inside the predicted Hermes
home before checking that the home was empty. Bundles with stamped env
that retained the explicit HERMES_HOME failed before Electron launched.

Resolve and admit the predicted home before any sandbox writes. Keep the
existing-profile refusal and provider seeding intact. Extend the seeding
contract for onboarding defaults and forced home clears.

Verified the old failure and fixed launch boundary on native Windows ARM64
with real driver imports and filesystem writes. Full package install/chat
acceptance still needs a fresh hosted smoke run.
2026-10-06 11:04:35 -04:00
ethernet
68718cf6d3 fix(release): record macOS channel artifacts without a release tag
Select the expected archive and embedded app version from the admitted
channel request, or the release tag for tagged builds. Channel builds
have no tag, so indexing it crashed before macOS packages could stage.
Keep filename, identity and provenance validation intact.

Regression coverage reproduces the crash on both architectures and checks
that wrong filenames, embedded versions and commits remain rejected.
2026-10-06 11:04:35 -04:00
ethernet
a0ebd22d57 fix(desktop): stream signing subprocess output 2026-10-06 11:04:35 -04:00
ethernet
39a487e6d7 test(desktop): drop progress-log assertions 2026-10-06 11:04:35 -04:00
ethernet
6b4346fc9c fix(desktop): show after-pack scan and signing progress 2026-10-06 11:04:35 -04:00
ethernet
36c70d31c7 fix(bundle): show progress while verifying the prepared payload 2026-10-06 11:04:35 -04:00
Austin Pickett
85db7c3a68 fix(dashboard): a resume target is (profile, session), not the session id alone
Session ids are only unique within a profile's store, and the keep-alive
registry is process-wide, so matching stranded PTYs on the key's resume
segment alone would let a resume in one profile close a detached PTY on the
same session id in another profile — somebody's reconnect window, with
whatever turn it had in flight. The key already carries the profile
segment; the resume arm now requires both to match the requested chat.
The lease-holder arm is unchanged: it reads the lease from the chat's own
home, so it already names the right process.

Found by Copilot's review.
2026-10-06 10:11:33 -04:00
Austin Pickett
f139d661c7 fix(dashboard): find the stranded PTY by lease ancestry, in the chat's own home
Two gaps kept the targeted release from firing on the reported repro:

- The lease pid is never the bridge's pid. ptyprocess forks the leader
  (`node entry.js`); the `tui_gateway` CHILD of that leader is what claims
  the single-writer lease, so an equality match on `bridge.pid` matched
  nothing in a real deployment (reporter's table: node 840, lease pid 850).
  `hosts_pid` now matches the leader itself or any PTY whose leader is an
  ancestor of the holder (psutil). A holder with no PTY ancestor — a plain
  `hermes` terminal — still closes nothing, so that refusal stands.

- Leases live in the home the chat runs in. A profile-scoped chat's child
  claims under the profile's HERMES_HOME, so reading the dashboard's launch
  home found no holder. The lookup now takes the child's HERMES_HOME.

The matched sessions are popped under the registry lock and closed outside
it (a close can wait out the helper SIGHUP grace and the lock serializes
every new chat), still before the spawn so the lease is released first —
the close discipline main's teardown series adopted, carried from #131179.

Co-authored-by: liuhao1024 <sunsky.lau@gmail.com>
2026-10-06 10:11:33 -04:00
Konstantin Khlopkov
226e557d28 fix(dashboard): release the PTY a rotated attach token strands
New chat rotates the browser's attach token, so the chat it left behind
stays registered under the previous token while its TUI child keeps the
single-writer lease on that session. close_other_sessions() is keyed on
the incoming token and never sees it; returning to the chat spawns a
second child that single-writer ownership refuses.

PtySessionRegistry.close_orphaned_sessions(resume, keep_key, holder_pid)
closes the stranded keep-alive PTY before the spawn: match by the key's
resume segment, or by the process holding the session's lease (a fresh
chat's key carries no resume target). A PTY with a live viewer is never
a candidate.

Carried from #131181 (squashed PR delta; original author preserved).
2026-10-06 10:11:33 -04:00
liuhao1024
2038bd92d2 test(todo): add atomicity and whitespace-merge tests per review feedback
Address @antifragileer's review suggestions on #44498:

1. test_partial_write_does_not_mutate_existing_list: pins that a failed
   write with mixed valid/invalid items leaves the prior list untouched
   (acceptance criterion #3 from #44496).

2. test_merge_new_item_rejects_whitespace_content: verifies whitespace-only
   content is rejected for new items in merge mode, guarding against a
   future code change that special-cases merge.
2026-10-06 09:53:34 -04:00
liuhao1024
ea61bb9ef8 fix(todo): reject empty content instead of substituting '(no description)' (#44496) 2026-10-06 09:53:34 -04:00