Commit Graph

31936 Commits

Author SHA1 Message Date
Teknium f8b87f5637 fix(goal): the judge only sees the goal's own background processes, and a pid/session wait barrier expires after 30 min
In a fan-out run the /goal loop parked for 3 h 22 min at the end on a
grandchild's poller (waiting_on_session=proc_21a6fe2369a1, 00:47 -> 04:09)
while the root itself had nothing running. Every one of the root's 7 logged
judge verdicts was WAIT on a child-owned process.

Two causes. gather_background_processes() was called with no task_id from
both goal-loop callers (CLI cli_loops_mixin, gateway run_goals), and the
registry's task_id is the CONTAINER key, which collapses to one value for
every agent in the process, so the judge's process list was every one of
~1,300 subagents' pollers. And a pid/session barrier had no ceiling: once
the judge said WAIT on a session that never exits, nothing resumed judging;
waiting_since was recorded and never read.

Now list_sessions() reports owner_task_id (the RAW spawning id the
registry already keeps for ownership checks), gather_background_processes
takes owner_task_id and both callers pass their own session id (CLI turns
register processes under self.session_id; gateway turns under
turn_ctx.session_id), and is_waiting() ages out a pid/session barrier
after _MAX_BARRIER_WAIT_S (30 min). Timed barriers keep their own deadline.

Tests: only the owning session's running processes are returned when
owner_task_id is given (unfiltered behaviour unchanged); a live barrier
older than the ceiling clears and judging resumes.
2026-09-05 00:30:19 -07:00
kshitijk4poor 5ac75e91e2 test(tools_config): assert display/runtime parity for disabled composite toolsets
The runtime-only assertion passed on unfixed main (it exercised
model_tools, not _get_platform_tools). Replace it with the contract the
fix exists for: a toolset is listed iff at least one of its tools survives
_select_tool_names.
2026-09-05 12:21:28 +05:30
kshitijk4poor 872be057e1 refactor(cli): class-level disabled_toolsets default instead of per-site getattr
Inspection surfaces run on partially built HermesCLI instances (tests use
HermesCLI.__new__), so the attribute needs a default; declare it once on
the class next to _seeded_first_message rather than getattr at five sites.
2026-09-05 12:21:28 +05:30
kshitijk4poor b1b8ea4f7d refactor(tools_config): single-pass pruning in _prune_toolsets_stripped_by_disabled
Resolve each remaining toolset once and state the keep-rule positively
(no static tools, or at least one tool survived the runtime subtraction)
instead of building a drop-set in a second resolve loop.
2026-09-05 12:21:28 +05:30
aydnOktay e2d738f89d fix(cli): guard disabled_toolsets on partial CLI instances
show_banner/_show_status call get_tool_definitions with disabled_toolsets,
but test and early-init paths build HermesCLI via __new__ without running
__init__, so self.disabled_toolsets was missing and CI failed with
AttributeError in test_cli_context_warning.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-05 12:21:28 +05:30
aydnOktay 2494eadb39 fix(tools): align inspection surfaces with disabled composite toolsets
hermes tools --summary and CLI /tools used name-level disabled_toolsets subtraction, so disabling debugging still showed terminal/web/file as enabled while runtime stripped those tools. Prune platform toolsets after tool-level subtraction and pass disabled_toolsets into CLI get_tool_definitions calls.

Fixes #97015

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-05 12:21:28 +05:30
kshitijk4poor ec1e6d50bc test(distributions): keep two invariant tests for grouped entries
Drop the literal-key assertion (change-detector) and the seeded 500-roll
loop (leaked global RNG state; vacuous if neither member rolls). Exercise
the fallback against a throwaway distribution so the contract no longer
depends on which browser_tasks entry has the highest probability.
2026-09-05 12:21:21 +05:30
kshitijk4poor d2664e9de1 docs(distributions): document compound entries once, at the module level
Move the grouped-roll rationale into the module docstring and shrink the
data-site comment; the mechanism is already explained on the sampler.
2026-09-05 12:21:21 +05:30
falkoro 6ba09c8cc3 fix(distributions): keep browser+web_search co-occurrence at 97% via grouped roll
Independent 97% rolls for browser and search co-occur only ~94% of the
time, versus 97% when web_search was bundled inside the browser toolset
(hermes-sweeper review). Grouped "+"-compound entries roll once and
select every member together; browser_tasks now uses "browser+search".
Deterministic tests cover the hit/miss/fallback/invalid-member contracts
and a 500-sample co-occurrence invariant.
2026-09-05 12:21:21 +05:30
falkoro decacbac35 fix(toolsets): stop disabled browser from stripping web_search
web_search belongs to the web/search toolsets, but the browser toolset
also listed it as a member. Because disabled_toolsets is a strict
end-of-pipeline subtraction (#17309), disabling browser — a natural
choice for headless/Docker deployments with no Chromium — stripped
web_search from every session even when web was enabled.

Remove web_search from the browser composite so its membership no longer
couples web search to browser availability. The browser_tasks RL
distribution kept web_search via browser, so add the search toolset there
to preserve its effective tool availability.

Fixes #64503
2026-09-05 12:21:21 +05:30
Teknium b51c055a12 fix(desktop): harden window-open deny and cover the link-title window
Follow-ups on the cherry-picked handler:
- a throwing observer can no longer change the decision; the handler returns
  an explicit deny regardless of logging failures
- the denied-URL log carries origin only, so query tokens / signed URLs from
  attacker-controlled content never reach the persisted desktop log
- the hidden link-title window (loads arbitrary user-linked pages on render,
  had no window-open handler at all) now denies too
- tests trimmed to two invariants (proven red against the pre-fix shape)
2026-09-04 23:19:11 -07:00
teknium1 77ca6a6d12 fix(desktop): deny window-open side-effect opens (GHSA-9f4c-93c8-jc8g)
setWindowOpenHandler opened details.url as a side effect before denying.
Per GHSA-9f4c-93c8-jc8g (CVE-2026-70608, High 7.2), a sandboxed iframe
with no allow-popups and no user gesture can reach this handler via the
OpenURL path -- and the desktop renders untrusted artifact HTML in
<iframe sandbox="allow-scripts">. A malicious artifact could therefore
force the OS browser to an attacker URL with zero interaction. Electron
ships no fixed 40.x release (fix is 41.10.3+/42.0.1), so we close it at
the seam, version-independently.

- electron/window-open-policy.ts: pure decideWindowOpen (always deny) +
  createWindowOpenHandler(onDenied) that denies and never opens a URL;
  the hook is logging-only.
- main.ts: wireCommonWindowHandlers uses it (covers primary + all
  secondary/quick windows); the deny is logged, no side-effect open.
- Trusted external links are unaffected: they already route through the
  audited hermes:openExternal IPC channel (openExternalUrl, http/https/
  mailto allowlist). Converted the one remaining bare window.open on the
  Electron path (env-var docs menu) to openExternalLink; other
  window.open sites are bridge-absent web fallbacks.
- tests-js/window-open-policy.test.ts: 4 tests pinning always-deny, the
  logging-only hook, and that a throwing hook never degrades to allow.
2026-09-04 23:19:11 -07:00
Sohom Sahaun 1e69c12b64 feat(slack): interactive Block Kit model picker for /model
Cherry-picked from PR #101542 by @sohomsahaun.

Two-step drill-down: provider static_select → model static_select,
with Back/Cancel buttons. Resolves through the same gateway callback
as Discord/Telegram, so persistence and failed-switch rollback
semantics come for free.

Changes:
- send_model_picker() posts Block Kit provider/model picker via chat_postMessage
- _handle_model_picker_action() dispatches provider/model/back/cancel actions
- _model_picker_state bounded dict (cap=100, workspace-scoped keys)
- Expired-state UX: missing state rewrites message to expiry notice
- Failure header: both exception and gateway error-prefix get 'Failed' header
- Index-based option values avoid Slack's 75-char value cap
- 21 tests covering send, action dispatch, gateway integration

Co-authored-by: Sohom Sahaun <ssahaun19@gmail.com>
2026-09-05 11:05:17 +05:30
kshitijk4poor f5832f81ed test(streaming): drive the Relay finalizer through its collector
The chat_completions finalizer now reads collector-observed chunks, not the
consumer loop, so the test feeds the captured on_chunk before calling it —
the same ordering Relay guarantees.
2026-09-05 10:54:19 +05:30
kshitijk4poor 74de0fd4fe fix(relay): build the chat-completions Relay response from collector-observed chunks
Relay invokes its finalizer as soon as the provider stream ends, concurrently with
Hermes' consumer thread, so any finalizer that reads the consumer loop's closures
races the last chunk. #103104 moved `usage` onto the collector; the same race still
truncated the final tool-call `arguments` delta, dropped the last content delta, and
downgraded `finish_reason` to "stop" in Relay's annotated response (reproduced ~50%
of runs). Every other Relay integration (Anthropic, Bedrock, Codex) already
rebuilds from collector-observed chunks; this makes chat_completions match via
`_RelayChatAccumulator` and removes the single-field nonlocal stopgap.

Test: the ordering harness is shared, and a second invariant test pins the
tool-call/finish_reason case (fails on the stopgap, passes here). Hermes' own
returned response was never affected.
2026-09-05 10:54:19 +05:30
mnajafian-nv 2cf5e68c5d fix(relay): retain usage from trailing chat completion chunks
Signed-off-by: mnajafian-nv <mnajafian@nvidia.com>
2026-09-05 10:54:19 +05:30
Austin Pickett 79445a496c fix(desktop): macOS parent-death watchdog treats ps: marker drift as inconclusive (#103172)
* fix(desktop): treat a macOS ps: parent-marker mismatch as inconclusive, not death

`ps -o lstart=` is a TZ/locale-rendered wall-clock string. Electron caches it
once per app lifetime; the backend re-renders it per spawn. A timezone change
(travel, or the DST boundary) while the app stays open makes the SAME instant
differ byte-for-byte, and _is_serve_orphaned() treated that as proof the parent
died -- os._exit(0) ~5ms after HERMES_BACKEND_READY, silently, on every respawn
until a full quit.

Route mismatches through _parent_start_marker_mismatch_is_conclusive(): linux:/
win:/winms: machine markers stay conclusive (PID-reuse defence intact); any ps:
side degrades to the PID-liveness check the legacy Desktop path already uses.
Log one warning before os._exit(0) so the exit is no longer traceless.

Fixes #95693
Fixes #93958

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

* fix(desktop): reject truncated ps: parent markers; blank marker env means absent

A ps: marker that got whitespace-split in env plumbing (`ps:Sat`) passed
_valid_parent_start_marker and armed a watchdog that could never match, so
the backend exited 0 right after HERMES_BACKEND_READY. Require a full
lstart value (>=4 tokens, a 4-digit year, a time). Treat empty
HERMES_PARENT_START_MARKER / HERMES_PARENT_NONCE as absent so a blank
inherited value degrades to PID-only tracking instead of disarming or
misfiring.

* chore: map contributor email for drewTuzson

* fix(desktop): log when the parent-death watchdog disarms on an unusable marker

Disarming is the fail-safe branch (the backend keeps serving) but it also
means this backend will never reap itself when the Desktop dies. Leave one
warning naming the rejected marker so that downgrade is not traceless.

* test(desktop): live macOS proof that a TZ-drifted ps: marker no longer kills the backend

Runs the real start_server in a subprocess against the host ps, with the
marker rendered under Europe/Paris and the backend under America/New_York.
Asserts READY + still alive after several watchdog polls, then that the
backend still exits once the stand-in parent is killed. Fails on main with
the reported symptom (exit 0, live parent); macos_only lane.

---------

Co-authored-by: ygd58 <buraysandro9@gmail.com>
Co-authored-by: Ahmett101 <Ahmett101@users.noreply.github.com>
Co-authored-by: Drew Tuzson <drew.tuzson@uqual.com>
2026-09-04 20:24:51 -04:00
Gille a99340c247 fix(dashboard): prevent PTY input from blocking event loop (#93565)
* fix(dashboard): prevent PTY input from blocking event loop

* fix(win-pty): don't terminate a healthy ConPTY on write cancellation; log leaked write workers

Review follow-up to the backpressure fix.

CancelledError on WinPtyBridge.write() ran the same path as a timeout and
force-terminated the ConPTY. Cancellation means the owning socket went away
mid-write, which is the keep-alive session's normal reattach case, not a
wedged child; killing the process there defeats the PTY-outlives-socket
design. Give the in-flight write the shutdown grace window and only
terminate if it never lands.

When terminate() fails to unblock pywinpty, the worker stays parked in the
default executor. That was swallowed by a bare except; log it so a slow
thread-pool starvation is diagnosable.

---------

Co-authored-by: Austin Pickett <pickett.austin@gmail.com>
2026-09-04 20:24:24 -04:00
Austin Pickett 04fd0172cd fix(dashboard): stop the embedded TUI repainting on every OS app-switch (#103165)
* feat(pty_bridge): mark the dashboard-spawned TUI with HERMES_PTY_HOST

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

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

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

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

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

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

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

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

---------

Co-authored-by: Raymond <supere989@users.noreply.github.com>
2026-09-04 20:24:02 -04:00
Teknium f159e581c7 feat(models): add GPT-6 Astra + Astra Pro with fast/flex speed tiers to Nous Portal and OpenRouter
Six slugs land in the nous and openrouter curated lists, above the gpt-5.6 line:
openai/gpt-6-astra{,-fast,-flex} and openai/gpt-6-astra-pro{,-fast,-flex}.

Nous Portal serves the tiers as distinct slugs (verified live: each echoes its id, service_tier
default/priority/flex, cost 1x/2x/0.5x). OpenRouter serves them as ENDPOINTS of the base model
(tags openai/fast, openai/flex) and silently routes an unknown suffix to the standard tier at
standard price, so the OpenRouter profile rewrites a tier slug to its base wire model and pins
provider.only to that tier's endpoints (OPENROUTER_ENDPOINT_PINS). The base slug is pinned to
openai/azure/azure-us so default routing never lands on a flex or fast endpoint.

Provider-agnostic metadata: one DEFAULT_CONTEXT_LENGTHS entry (gpt-6-astra: 1,050,000, live on
OpenRouter for both models; substring-matches -pro and the tier suffixes). Pricing is skipped:
both routes bill via official_models_api. Reasoning floor not added (no evidence of long thinks).
2026-09-04 15:58:59 -07:00
hermes-seaeye[bot] 71f8c60f6a fmt(js): npm run fix on merge (#103240)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-09-04 22:20:13 +00:00
Hao Zhe 64534b4dd4 fix(desktop): use shared tooltip for listing gallery 2026-09-04 15:14:49 -07:00
Brooklyn Nicholson b0ab2e163a test(desktop): pin that a multi-tab zone can still hide its strip
The resolver's own suite only ever asserted `never` against a LONE pane, so
the whole-stack stranding check passed every existing case while Hide tabs was
dead in the one zone users keep tabs in. Adds the missing count: an explicit
`never` is honored once a closeable tile has a neighbor to cycle to, and those
same stacks still paint their strip on auto (the rung resolves before `mode`,
so a stack it stops claiming has to reach the auto rule).

Verified as a tripwire — reverting the resolver fails the first case.
2026-09-04 15:01:10 -05:00
Brooklyn Nicholson 2c52945974 fix(desktop): Hide tabs works again in a zone holding more than one tab
The reachability rung that outranks an explicit `never` ran over the whole
stack: any closeable `placement: 'main'` pane pinned the strip visible at any
tab count. Session tabs and bot chats are exactly that, so one session tab in
main was enough to make Hide tabs a no-op — the zone menu row and ⌘⌥T both
wrote `tabStrip: 'never'` and the resolver overrode it on the next line.

Stranding is about the last handle, not about tabs existing. A stack of two or
more answers tab cycling and ⌘1…⌘9, so hiding its strip costs chrome and no
handle. The tile and tool-panel rungs now apply to a lone pane; hide-only
chrome (sessions / Bots) keeps firing at every count, since it has no close
verb and its Show/Hide rows live on the strip itself.
2026-09-04 15:01:10 -05:00
Brooklyn Nicholson 4b30b91756 Teach the desktop agent the listing fence
One clause in the desktop platform hint, covering the field shape and that
it applies to every property presented — including follow-ups and
re-rankings, so a shortlist stays comparable instead of decaying into prose
after the first answer.
2026-09-04 15:00:43 -05:00
Brooklyn Nicholson cb6cb74696 Keep renderer-owned fences out of artifact promotion
Artifact detection runs before the rich-fence registry, so a fence long
enough to look like a code document was promoted to a code card and never
reached its own renderer. A multi-listing set crosses that threshold
easily, which made the same content render two different ways depending on
how many properties it held.

`listing` joins the languages that are never artifacts, next to `mermaid`
for the same reason.
2026-09-04 15:00:43 -05:00
Brooklyn Nicholson 9b16719e17 Render property listings as browsable transcript cards
Adds a `listing` fence to the rich-embed registry: JSON in, a native card
out — photo mosaic, address and price, specs, and the catches worth
verifying before a tour.

The rental portals publish no embeddable surface (public APIs deprecated or
broker-gated, no oEmbed, X-Frame-Options on every listing page), so a URL
cannot be framed. The data the agent already gathered can be, and the
portals' image CDNs hotlink fine — so the card is authored rather than
fetched, alongside the existing mermaid and svg fences.

Parsing treats every field as untrusted model output: one object, a bare
array, or `{listings: []}` all work, beds accept `3` and `"3.5"`, non-http
URLs are rejected so a card cannot smuggle a javascript: href, and lists
are capped. Anything unparseable falls back to the plain code block.
2026-09-04 15:00:43 -05:00
abundantbeing 6b1e12c7f4 feat(desktop): drag to create sessions from New session, project + controls, and profile groups 2026-09-04 14:57:37 -05:00
Brooklyn Nicholson 96e1e3f921 feat(desktop): Message Bubble transparency slider in Appearance
One 0-100 lever beside Window Translucency: 0 keeps your bubble solid (the
default), 100 leaves only the outline. The store paints a single root var,
--user-bubble-keep, that --dt-user-bubble mixes in, so the read-only bubble
and the inline edit composer follow together and an untouched lever yields
byte-identical CSS. Reuses TranslucencySlider (with its overlay peek), has a
Cmd-K search entry, and copy in all six locales.
2026-09-04 14:40:54 -05:00
Brooklyn Nicholson 59302ece0d fix(desktop): transcript timestamps were painted at 30% ink in both modes
TimelineTimestamp layered text-muted-foreground/55 on a token that is already
a 54% mix of the base ink, so the stamp rendered near 30% alpha: 2.3:1 on a
dark chrome, 1.9:1 on a light one. Point it at --conversation-scaffold-meta,
the token the tool-row stamps already use, so every stamp in the transcript
reads from one place.
2026-09-04 14:40:54 -05:00
kshitijk4poor 5a0b1ba766 test(local-models): assert _assign_default reaches the real assignment body
Follow-up to the salvaged fix from #102994. The quickstart leg tests
stubbed web_deps.late itself, so the resolution path that regressed
(late() -> web_server facade -> getattr) was never exercised; the PR's
proposed tests pinned the module string instead, which fails on any
future move of the symbol even when the call site is updated.

Stub only the leaf (web_server_config._apply_model_assignment_sync) so
the real late() runs, and add one invariant test: _assign_default must
call it with ("main", "llamacpp", model_id, "", "", ""). Red on base
(AttributeError), green with the fix; not tied to where the symbol lives.
Drop the docstring parenthetical that described test mechanics.
2026-09-05 00:29:42 +05:30
Leanolf aef409343d fix(local-models): point _assign_default's late-bound model assignment at web_server_config
The whole-codebase refactor moved _apply_model_assignment_sync from the
web_server facade into web_server_config, but _assign_default in
web_routers/local_models.py still resolved it through the default late()
module (web_server), which no longer carries the symbol. A local-model
quickstart therefore failed its final 'making it your default' step with
AttributeError: module 'hermes_cli.web_server' has no attribute
'_apply_model_assignment_sync'.

Pass the defining module explicitly to web_deps.late() so the call
resolves against hermes_cli.web_server_config, matching how the sibling
models.py router imports the same helper. Update the two quickstart test
stubs to accept the module argument the real late(name, module=...) takes.
2026-09-05 00:29:42 +05:30
Nacho Avecilla a6e10e693f fix(gateway): stop dropping the first message after an Azure agent sleeps (#99736)
* feat(gateway): let NAS broker the scale-to-zero suspend where the guest has no lever

* fix(gateway): require the going_idle ack and outlast the broker before suspending

* fix(gateway): release the redial hold on every path that abandons the suspend

* fix(gateway): hold the re-dial only for the brokered lever, and require it

* fix(gateway): make the re-dial hold contract real and fence the in-guest suspend too

* refactor(gateway): share the watcher-iteration and descriptor helpers across the sleep tests

* refactor(gateway): move the sleep tests' repeated setup into their fixtures

* refactor(gateway): one docstring line per sleep test, and parametrise the abort and lever paths

* fix(gateway): fence the freeze gap, cool down aborts, and stop clobbering a shutdown drain

* fix(gateway): slice the Fly freeze fence on the wall clock and widen it for larger machines
2026-09-04 15:41:27 -03:00
hermes-seaeye[bot] 59d330fd15 fmt(js): npm run fix on merge (#103110)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-09-04 18:28:32 +00:00
Brooklyn Nicholson 1d1411df9c fix(desktop): keep the saved skin name until the registry can resolve it
normalizeSkin ran at boot, before the gateway seeded backend skins, and flattened an unresolvable name to the default for the whole session. The connect-time seed is apply: false by design, so nothing repainted. Keep the raw pick in provider state and normalize against the live registry instead.
2026-09-04 13:19:19 -05:00
Brooklyn Nicholson 413d5ab863 fix(desktop): cache backend skins so the boot paint can resolve the saved one
$backendThemes only lived in memory, so a relaunch on a skin from ~/.hermes/skins painted the default until the next skin.changed. Hydrate the registry from localStorage like user themes, and write it back on every change.
2026-09-04 13:19:19 -05:00
Brooklyn Nicholson 9403ad2656 refactor(desktop): share the theme shape check between registries 2026-09-04 13:19:19 -05:00
Brooklyn Nicholson 433f719676 fix(desktop): tab ✕ fade lands on the visible field under Glass
Under Window Translucency → Glass, the glass block sets the surface
tokens transparent so <body> is the sole painter. A tab's `--tab-face`
read those tokens, so the ✕ runway resolved to transparent→transparent
and the chip's `bg-(--tab-face)` painted nothing: hovering showed a bare
✕ over un-faded text, with no gradient at all.

Add a `data-glass-field` contract beside the block's existing
`data-glass-opaque` / `data-glass-raised`: a declarer receives
`--glass-field`, the same body mix glass already paints. The tab prefers
it and falls back to its own surface token (`--tab-surface`, which
TAB_ACTIVE / TAB_IDLE now set instead of `--tab-bg` so they feed the
chain rather than bypass it). Face, hover-darken, and the selection wash
all inherit the one base. Inert with glass off: the fallback resolves to
the exact opaque surface it did before.
2026-09-04 13:09:07 -05:00
Brooklyn Nicholson ff75ff2a18 fix(desktop): closeable tab labels clip so the ✕ fade has text to dissolve
The label span used `truncate`, whose ellipsis stops the text ~8px short
of the overlay. The hover gradient then faded over empty tab surface —
surface to the same surface, invisible by construction — and the ✕ read
as floating in a gap beside three dots.

A closeable tab publishes `data-closeable`; the label reads it via
`group-data-[closeable]/tab:text-clip` so overflow runs under the fade
and the gradient dissolves real letterforms. Vertical rails and
uncloseable tabs have no ✕, so they keep the ellipsis.
2026-09-04 13:09:07 -05:00
Brooklyn Nicholson 87e78d668b fix(desktop): pane tab ✕ costs a min-width floor, not 36px of dead padding
#96880 reserved `pr-9` on every closeable tab so the hover ✕ could never
sit over a label — 36px of empty runway at rest on every tab, nearly
doubling FILES (40→76px) and taxing tabs whose label already cleared the
overlay by a mile.

Restore the overlay as designed (✕ paints over the label's right edge,
gradient-blended, zero layout shift) and protect short labels with a
`min-w-13` floor instead. Sized from the chrome it has to survive: 8px
label inset + the ~19px opaque chip, so the shortest labels clear the
chip and pass only under the translucent fade. A floor bills only tabs
that need it; TERMINAL, BROWSER, NEW CHAT pay nothing.

The test that pinned `pr-9` by class string is rewritten as a set-diff
contract: adding `onClose` must add a `min-w-*` and never a `pr-*`.
2026-09-04 13:09:07 -05:00
Brooklyn Nicholson d353931dd9 test(desktop): assert tooltip labels stay in inline flow, not inline-flex
The preview-row and terminal-rail tests pinned the label's `inline-flex`
class — the very shape that broke multi-line tips. Assert the invariant
instead: no flex box under the decoration, and the preview row splits its
two lines with a hard break.
2026-09-04 12:51:08 -05:00
Brooklyn Nicholson a22b295dbd fix(desktop): paint every line of a multi-line tooltip, not just the first
Tip's chip is an inline span with `box-decoration-break: clone`, so its
background only paints the line boxes of inline flow. The #62022 hardening
forced every label child to `inline-flex`, which is an atomic inline: it sits
on the baseline and hangs any extra lines below the chip, dark-on-dark. The
preview status row's two-line label (target path + click hint) showed the
path lit and the hint invisible, and long paths wrapped into unreadable rows.

Force label children `inline` instead — still no block collapse, and wrapped
content stays in flow. TipHintLabel and the preview row drop their flex
boxes for plain inline text (a `<br />` for the hard break).
2026-09-04 12:51:08 -05:00
Teknium 13e72fb205 Merge pull request #102986 from NousResearch/fix/browser-exec-packaged-chromium
browser_exec local mode drives the packaged Chromium instead of the user's Chrome
2026-09-04 08:42:27 -07:00
Teknium 9e13ac2f01 fix(browser): browser_exec local mode drives the packaged Chromium, not the user's Chrome
Symptom: with Browser Use mode (the default) and no cloud provider / CDP
override, browser_exec left BU_CDP_* unset, so the browser-use harness ran
its own local discovery — hunting for the user's INSTALLED Chrome on its
default profile. That path needs the chrome://inspect remote-debugging
toggle plus an "Allow remote debugging?" popup per run, is blocked outright
on Chrome >=136, and on a headless host (or one without Chrome) fails with
"chrome-not-running: no supported Chromium-family browser is running". The
built-in browser_* tools never had this problem: they drive the Chromium
Hermes installs, launched through agent-browser.

Change: `_resolve_backend_cdp` step 4 becomes the local ENGINE —
lightpanda when configured, otherwise `_resolve_managed_chromium_cdp`,
which runs `agent-browser --session <key> get cdp-url` through the legacy
`_run_browser_command` (Chromium preflight/auto-install, per-key session
cache, inactivity reaper, atexit) and exports the returned ws:// endpoint
as BU_CDP_WS. Each cache key (task, or bu-named-<session>) gets its own
Chromium, so named sessions are private and skip the own-tab preamble.
Running `get cdp-url` on every call also refreshes agent-browser's idle
timer (it never sees the harness's direct CDP traffic) and follows a
relaunch after the reaper closed the browser.

Live: before, browser_exec on origin/main → exit 1 "chrome-not-running";
after → new_tab/page_info succeed on a cold start, warm reuse, post-reap
relaunch, and a named session; a missing Chromium surfaces the install
hint instead of the harness's Chrome hunt.
2026-09-04 08:30:32 -07:00
Teknium 8924b3aed9 fix(skills): lead the lesson-layer contract with the primary purpose — how to do the task, to the user's specifications 2026-09-04 08:15:22 -07:00
Teknium c240e65399 fix(skills): self-improvement writes lessons, not incident logs
The skill review fork, the combined memory+skill review, and the curator's
consolidation pass all described references/ as the place for
"session-specific detail", and the curator's demote step said to move a
sibling's file under the umbrella. Followed literally over months that
produced one dev skill with a 100k SKILL.md dense in PR numbers and 443
one-per-session reference files, plus five sibling skills restating the
same rules and the repo's AGENTS.md.

The three prompts now share one shape contract: an entry is an imperative
rule plus one clause of why, stated once; no PR/issue numbers, dates, or
quoted chat as content; references/ is a small topical set extended in
place, never a per-session file; skills do not restate always-loaded
context. Consolidation is defined as distilling, and copying a sibling
verbatim under references/ is named as the failure. skill_manage's schema
carries the one-sentence version.

Two advisory linter rules make the shape visible in the tool result the
moment it starts to drift: incident-log-shape (PR/issue-number density in
prose) and references-sprawl (>60 reference files), the latter also run
on references/ writes. On the real before/after: the old skill trips both,
the consolidated one trips neither.
2026-09-04 08:15:22 -07:00
Teknium f1ccf436a2 feat(web): Perplexity Search API as a web_search + web_extract backend
Adds plugins/web/perplexity — a keyed-only WebSearchProvider over httpx:

- search: POST https://api.perplexity.ai/search (documented Search API),
  search_context_size=low so `snippet` stays description-sized;
  results[].snippet -> description, max_results capped at the API's 20.
- extract: POST /sdk/content/snippets — the query-relevant page-excerpt
  route behind `pplx content snippets` (the CLI's `content fetch` is
  deprecated upstream). web_extract has no query, so the URLs' path words
  serve as the relevance query; per-URL `error` entries survive a 200.
- Wired into the same touchpoints as the other keyed vendors: legacy
  backend set + credential ladder + availability probe (web_tools),
  registry preference walk, OPTIONAL_ENV_VARS, `hermes config`/status/
  dump key lists, nous_subscription direct-credential detection, setup
  summary, test conftests, docs.

Not a keyless-ring member (Perplexity has no anonymous tier). Related
closed PRs #9192 / #23981 / #45225 predate the plugin ABC.
2026-09-04 07:17:00 -07:00
Teknium d3630f8532 refactor: whole-codebase simplification — −34% source LOC, every god file decomposed, zero behavior change (#102117)
refactor: whole-codebase simplification — −34% source LOC, every god file decomposed, zero behavior change
2026-09-04 05:50:52 -07:00
Teknium 957710a119 fix(plugin-compat): scan the plugin's real package, never the CWD; override needs literal true
The compat scanner derived a directory from manifest.path by splitting at the
first ':' and falling back to .parent when the result was not a dir. An
entry-point plugin (`vendor_plugin:register`) and a Windows path
(`C:\Users\...`) both collapsed to '.', so a stray .py in the launch directory
was attributed to the plugin and the installed package was never scanned.
After the removal date that disables the wrong plugin. `_scan_root()` now
takes directory manifests verbatim and resolves entry points via
importlib.util.find_spec to the installed package dir; anything unresolvable
scans nothing.

`plugins.allow_deprecated_imports` used bool(), so the YAML string "false"
opened the post-removal bypass. It now requires the literal boolean True, and
summary_lines() says "force-loaded" instead of "DISABLED" when the override is
what kept the plugins running.

Reported-by: ayushnangia (PR #102117 review)
2026-09-04 04:56:11 -07:00
Teknium 38742342b2 fix(auth): qwen OAuth login reaches _mark_qwen_oauth_active in its moved owner
The compat sweep dropped the auth-facade re-export of _mark_qwen_oauth_active
while hermes_cli/auth_commands.py still reached it through the auth_mod module
alias, so every 'hermes auth add qwen' died with AttributeError on this branch.
Import it from hermes_cli/auth_qwen where it now lives. Module-alias attribute
reads dodge import-time checks; the sweep in the PR thread found this to be the
only production hit of that class.

Reported-by: yoniebans (PR #102117 review)
2026-09-04 04:37:08 -07:00