Commit graph

18147 commits

Author SHA1 Message Date
teknium1
d6fa2709de feat(cli): /focus — reduced-output view with hidden-line recovery and status indicator
Display-only port of Claude Code /focus; composes with existing /verbose tool-progress modes.
2026-07-26 18:10:34 -07:00
brooklyn!
e1ace0ac98
Merge pull request #72336 from NousResearch/bb/statusbar-prefs
Quieter status bar and sidebar counts
2026-07-26 20:10:16 -05:00
brooklyn!
6cea77303b
Merge pull request #72339 from NousResearch/bb/redirect-user-row
Preserve the original prompt when a mid-turn redirect corrects a turn
2026-07-26 20:09:46 -05:00
teknium1
a9cc0ac9ff fix(i18n): add the /context catalog block to ar.yaml (missed in the 17-locale sweep) 2026-07-26 18:06:21 -07:00
teknium1
07370a9dba feat(cli,gateway): unify /context into a visual context-usage breakdown
Extends the cherry-picked /context command (PR #52184) and prompt-size
attribution helpers (PR #66656) into one visual context view across
surfaces, and absorbs the per-component budget-visibility goal of the
/tokens proposal (PR #48470):

- agent/context_breakdown.py: pure renderers over the existing payload —
  a 5x20 glyph block grid (1 cell ~= 1% of the model window), an
  'Estimated usage by category' table with free space, and expanded
  per-skill / per-toolset listings via compute_context_details(), which
  reuses the prompt-size attribution mechanism (skills index-line bytes +
  registry tool->toolset map) converted to the same chars/4 heuristic.
- cli.py: /context [all] renders grid + category table (+ expanded
  listings) from the live agent and in-memory conversation history.
- gateway/slash_commands.py: /context appends the plain-text category
  table (no grid — monospace not guaranteed on messaging platforms);
  /context all adds the expanded listings. Fail-open: breakdown errors
  never break the gauge.
- hermes_cli/commands.py: /context gains the 'all' subcommand; /version
  demoted to /hermes version on Slack to keep the 50-slash cap.
- tests: renderer unit tests against synthetic payloads, registry test,
  gateway /context + /context all + failure-degradation handler tests.
- docs: slash-commands reference + CLI guide entries.

Read-only and locally computed: no provider calls, no prompt-cache impact.

Co-authored-by: RemyFevry <29257684+RemyFevry@users.noreply.github.com>
Co-authored-by: joelbrilliant <joelbrilliant1@gmail.com>
Co-authored-by: CharlesMcquade <6466275+CharlesMcquade@users.noreply.github.com>
2026-07-26 18:06:21 -07:00
joelbrilliant
8b3da145f1 fix(prompt-size): include names-only skills in breakdown 2026-07-26 18:06:21 -07:00
joelbrilliant
8b9423444e feat(prompt-size): per-skill and per-toolset token-cost breakdown
`hermes prompt-size` reported skills as one <available_skills> block total
and tools as one json-bytes total, so there was no way to see which
installed skill or toolset actually dominates the fixed prompt budget.

Add two additive breakdowns to compute_prompt_breakdown (hermes_cli/
prompt_size.py):

- toolsets_breakdown: each resolved tool is attributed to its single
  canonical registry toolset (registry.get_tool_to_toolset_map), summed by
  group. Fully attributable — the grand total equals the existing
  tools.json_bytes minus JSON array framing (2*count bytes).
- skills_breakdown: parsed from the rendered <available_skills> block, one
  entry per skill with two honest, distinct numbers — index_line_bytes (the
  always-on cost of listing the skill) and skill_md_bytes (on-disk SKILL.md
  size, the real read cost paid only on skill_view). Sorted largest-first
  by read cost.

render_breakdown prints both as sorted "Toolsets by size" / "Skills by
size" tables (skills capped at 20; --json carries them all). All existing
keys and output are unchanged.

Runs fully offline (dummy credentials, no network). Tests cover shapes,
largest-first ordering, per-tool attribution reconciling to the total,
namespaced-name parsing, and unmapped-skill handling.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-26 18:06:21 -07:00
CharlesMcquade
4fe2fecf54 feat(gateway): add /context command for a detailed context-window view
A dedicated /context (alias /ctx) gateway slash command that gives a full
context-window view with:

- Usage gauge: visual bar + fraction + percentage + headroom
- Auto-compression threshold and how far away it is
- Compression count and how much the last one freed
- Cumulative session throughput (explicitly labelled as throughput,
  NOT context size — each call re-sends the window)
- Cascading fallback: running agent → cached agent → SessionStore metadata
  → rough transcript estimate

Not included (per current-main design):
- Cache reporting removed: commit 446b8e239 intentionally removed cache
  reporting from user-facing surfaces because providers that omit cached-token
  details produce misleading values
- Sync DB calls replaced with async_session_store (current main requires
  AsyncSessionStore with await)

Also rewords the /status tokens line from 'Cumulative API tokens (re-sent
each call)' to 'Lifetime tokens billed: ... (not your current context size;
use /context)' to reduce the recurring confusion that the cumulative figure
is the current context window.

Fixes salvation of PR #52184 (salvage commit replaces a 12K-commit-behind
fork branch with a fresh implementation against current main, incorporating
reviewer feedback from @whoislikemiha and the hermes-sweeper).
2026-07-26 18:06:21 -07:00
teknium1
3dccc45032 docs(portal): describe per-model routing accurately — not everything goes through OpenRouter
The Nous Portal docs claimed routing 'happens through OpenRouter under
the hood' with OpenRouter-equivalent failover, and that the catalog
'mirrors OpenRouter's model list'. That is not the Portal's contract:
some models route through OpenRouter, others through proprietary or
secondary providers, and per-model routing can change over time.

The stale wording licensed users to expect OpenRouter-proprietary
request extensions (top-level cache_control, session_id sticky
routing, provider preferences) to work through the Portal, producing
misfiled bug reports like #71576. Reworded both pages (en + zh-Hans)
and added an explicit note that OpenRouter-specific extensions are not
part of the Portal API contract.
2026-07-26 18:02:13 -07:00
teknium1
a0112ef26e feat(approvals): consecutive-denial circuit breaker for smart approvals
After N consecutive guardian denials in a session the deny message escalates to a hard-stop instruction. Inspired by ChatGPT Work auto-review circuit breaker.
2026-07-26 18:02:04 -07:00
Brooklyn Nicholson
c5336b472e feat(statusbar): right-click to choose what the bar shows
The status bar shipped every affordance it had, so approvals, the terminal
toggle, agents, cron and webhooks sat there permanently for users who never
touched them.

Those five now start hidden and the bar owns a context menu that turns them
back on, persisted per install. Items opt in by naming themselves with
`toggleLabel`, so a plugin contribution that doesn't opt in always shows;
the system icon and the version/update pills are listed but locked on,
since hiding the way back into settings strands the user.

Preferences store the hidden set rather than the visible one, so an item
added to the bar in a later version appears for existing users instead of
staying silently off.
2026-07-26 19:52:57 -05:00
teknium1
85c2976e22 fix(update): migrate legacy pythonw Windows gateway launchers to the hidden-console design
Two halves close the 'legacy pythonw gateways survive updates forever' gap:

1. hermes update now regenerates the installed Scheduled Task / Startup
   launcher scripts (gateway.cmd + gateway.vbs) during the gateway resume
   phase. They are persistence artifacts written once at install time;
   updates never touched them, so pre-aa2ae36c3f installs kept launching
   the gateway through pythonw.exe forever — every descendant spawn
   flashed a conhost (#54220/#56747) and, since #70344, the console-less
   gateway died at startup with RuntimeError: sys.stderr is None (#71671).
   The task /TR points at a stable script path, so rewriting the files
   retargets it with no schtasks call and no UAC. No-op for modern
   installs; best-effort so a failed refresh never fails the update.

2. _resolve_detached_python() normalizes a legacy pythonw.exe interpreter
   to its sibling console python.exe when it exists, so the update
   pause/resume argv-replay path (and any other caller handed a legacy
   command line) respawns on the current design instead of faithfully
   resurrecting the old one. Keeps pythonw when no sibling exists — a
   failed respawn is worse than a console-less gateway.
2026-07-26 17:50:54 -07:00
teknium1
1e652cca7a fix(cli): register 'approvals' in _BUILTIN_SUBCOMMANDS (startup plugin-gating parity) 2026-07-26 17:49:03 -07:00
teknium1
db90e36202 feat(approvals): hermes approvals suggest — mine approval history into allowlist proposals 2026-07-26 17:49:03 -07:00
teknium1
d6fdae6746 feat(desktop): quick-entry window — global hotkey capture to any session
Parity with Claude Desktop quick-entry-window / ChatGPT Quick Chat.
2026-07-26 17:48:19 -07:00
teknium1
aa3121bb34 feat(desktop): Quick Entry — global-hotkey mini composer window 2026-07-26 17:48:19 -07:00
teknium1
5557b10fb6 fix(desktop): tear down find-in-page highlights on navigation and pin the keybind registration
Electron's findInPage selection is per-webContents, not per-route: without
an explicit teardown, the highlight overlay (and a stale match counter)
from one chat survived navigating to another session or a settings page.
The FindBar now closes itself via effect cleanup keyed on the router
pathname — the first render never fires it, a route change tears down the
previous route's search, and unmount (the session/profile switch paths
that remount the global overlays) gets the same teardown. closeFindBar is
already idempotent, so a closed bar never re-enters the bridge, and a
navigation with the bar closed makes zero bridge calls.

Tests (find-bar.test.tsx 42 -> 48):
- navigation closes the bar, resets the store, and calls
  stopFindInPage exactly once (highlight cleanup on session switch)
- navigation with the bar closed never reaches into the bridge
- keybind registration pins: view.findInPage is mod+f in the view
  category; mod+f passes comboAllowedInInput (so ⌘F fires from the
  composer instead of typing 'f'); the find-next/find-previous pair
  ships unbound while view.toggleReview keeps mod+g; all three actions
  have en + zh labels for the keybinds panel

The component tests now render inside a MemoryRouter (FindBar reads
useLocation), with a navigation harness that captures useNavigate.
2026-07-26 17:48:05 -07:00
teknium1
6d427e82df feat(desktop): complete find-in-page with the find-next/find-previous accelerators
PR #53891 (cherry-picked in the three preceding commits) landed the Electron
bridge, the store, the find bar overlay, and Cmd/Ctrl+F to open. It stopped
short of the rest of the accelerator set and had two lifecycle leaks. This
completes the surface to match the platform convention that Chrome, Safari,
VS Code, and Claude Desktop's own findInPage bundle all ship.

Accelerators now wired:
- Cmd/Ctrl+F      open the find bar          (view.findInPage, from the PR)
- Cmd/Ctrl+G      find next                  (new)
- Cmd/Ctrl+Shift+G find previous             (new)
- Enter / Shift+Enter step from the input     (from the PR)
- Escape          close + stopFindInPage('clearSelection')  (from the PR)

Keyboard ownership (the substantive fix)

Cmd+G was already bound to `view.toggleReview` and Escape to
`composer.cancel`. The find bar's own capture-phase window listener cannot
win those keys by calling stopPropagation: the keybind dispatcher's listener
sits on the SAME window target in the SAME phase, and propagation control
does not suppress sibling listeners on one target. Left alone, Cmd+G would
step a match AND toggle the review pane, and Escape would dismiss the bar AND
abort a running turn.

So ownership is decided by the dispatcher, which AGENTS.md already names the
single owner of combo dispatch: `findBarClaimsCombo` is consulted in
use-keybinds before the registry lookup, and yields mod+g / mod+shift+g /
escape to the bar only while it is open. Closing the bar hands every one of
them straight back. That is the "keyboard ownership follows focus / one cancel
gesture does exactly one thing" invariant.

`view.findNext` / `view.findPrevious` are registered with EMPTY defaults on
purpose — shipping mod+g as a second default would flag a permanent conflict
in the keybinds panel against view.toggleReview. The entries document the pair
and let a user bind a dedicated chord; stepping is a no-op unless the bar is
open with a query, so a bound key can never search invisibly.

Listener-leak fixes

- The found-in-page bridge subscription is now refcounted in the store, so a
  remount (the connection re-home path remounts the global overlays) cannot
  stack duplicate subscribers that each re-dispatch the same result and
  outlive their component. The subscription is deliberately mount-scoped, not
  active-scoped: results for an in-flight search must still land if the bar
  just closed.
- `setFindQuery` now refuses to search a closed bar. The component clears its
  debounce on close, but a 200ms timer that already fired would re-issue a
  find and re-highlight the page after the user pressed Escape. Caught by the
  test, fixed in the store rather than papered over in the component.
- `closeFindBar` is idempotent — Escape is a shared gesture, so a second close
  must not reach into Electron again.

Pure logic extracted for testing (no source regexing)

- `src/lib/find-in-page.ts`: `formatMatchLabel` (three distinct counter
  states: hidden with no query, explicit 0/0, ordinal/count; clamps the
  ordinal and never emits NaN — Electron legitimately reports ordinal 0 on a
  non-final update), `findBarKeyAction` (the keybinding matcher, DOM-free),
  and `findBarClaimsCombo` (the ownership predicate above).

Also: match counter and buttons get accessible names and the counter is
aria-live, the hardcoded English "Previous"/"Next"/"Close" tooltips move to
i18n (en + zh) alongside the new keybind labels, and the input gets an
aria-label so the bar is reachable by role.

Tests: apps/desktop/src/components/find-bar.test.tsx — 42 cases over the
pure helpers, the store (open/close, next/prev dispatch shape, escape clears,
refcount, double-release), and the component (focus on open, debounce
coalescing, Cmd+G from outside the input, unmount releases both the bridge
subscription and the window listener).

  cd apps/desktop && npx vitest run src/components/find-bar.test.tsx \
    electron/find-in-page.test.ts
  -> 62 passed (42 new + 20 from the PR)

Adjacent suites (src/lib/keybinds, src/i18n, src/store): 487 passed.
`tsc -p tsconfig.electron.json --noEmit` clean; `tsc -p .` has 114 pre-existing
errors vs 120 on the merge base (all @assistant-ui / bippy / composer), none in
the touched files. eslint clean on every touched file.

Co-authored-by: David Metcalfe <DavidMetcalfe@users.noreply.github.com>
2026-07-26 17:48:05 -07:00
David Metcalfe
31385a0102 test(desktop): cover find-in-page helpers and multi-window routing
Vitest coverage for apps/desktop/electron/find-in-page.ts.
The helpers and the IPC handlers in main.ts are the only
consumers, so the tests pin the wire shape (match counter
shape, options defaults, no-throw-on-destroyed) and the
multi-window correctness that the original CJS PR missed:

- formatFoundInPage:
  - Maps { activeMatchOrdinal, matches } → wire payload.
  - Coerces missing fields to zero (the renderer never
    sees NaN).
  - Tolerates null / undefined inputs.

- performFind:
  - Forwards query + options to webContents.findInPage.
  - Defaults forward=true and findNext=false when omitted.
  - Treats null / non-object options as "all defaults".
  - Coerces a non-string query to string (defensive against
    a misbehaving renderer).
  - Is a no-op on null webContents.
  - Is a no-op on destroyed webContents (does not throw
    across the IPC boundary).

- stopFind:
  - Calls stopFindInPage with the default action
    ('clearSelection').
  - Honors an explicit action argument.
  - Is a no-op on null or destroyed webContents.

- installFoundInPageForwarder:
  - Forwards 'found-in-page' to the sender as a formatted
    payload.
  - Handles missing fields without throwing.
  - Skips send when webContents is destroyed at fire time.
  - Returned uninstall removes the listener.
  - Returned uninstall on null/destroyed webContents is a
    safe no-op.
  - Regression: two forwarders installed on distinct
    webContents do not cross-fire. This is the bug the
    original PR shipped — the global mainWindow listener
    routed results to the primary regardless of which
    renderer invoked findInPage. Pinning this here keeps
    the per-sender routing from regressing.

Run with:
  cd apps/desktop && npx vitest run electron/find-in-page.test.ts --project electron
2026-07-26 17:48:05 -07:00
David Metcalfe
43e86ea50e feat(desktop): wire find-in-page in the renderer
Brings forward the renderer-side changes from PR #53891,
adapted to the current main branch (where app-shell.tsx
has been replaced by apps/desktop/src/app/contrib/wiring.tsx
and keybinds/actions.ts has gained new view.* entries):

- apps/desktop/src/store/find-in-page.ts (new): nanostores
  atom + actions for the find bar (openFindBar, closeFindBar,
  setFindQuery, findNext, findPrevious, updateFindResults,
  initFindInPageListener). openFindBar is dispatched by the
  view.findInPage keybind handler in use-keybinds.

- apps/desktop/src/components/find-bar.tsx (new): the find
  bar overlay (top-right, below the titlebar). Debounces
  input 200ms before issuing findInPage, focuses on open,
  supports Enter (next) / Shift+Enter (previous) / Escape
  (close), shows a "3/12" match counter from the
  'hermes:found-in-page' stream. Global capture-phase
  Escape listener so the bar closes regardless of focus.

- apps/desktop/src/lib/keybinds/actions.ts: adds
  view.findInPage with default combo 'mod+f'. The keybinds
  runtime already routes any mod+ / ctrl+ combo through
  editable-focus contexts (see comboAllowedInInput in
  lib/keybinds/combo.ts:193), so ⌘F focuses the find bar
  instead of typing 'f' into a textarea — matches browser
  behavior.

- apps/desktop/src/app/hooks/use-keybinds.ts: wires
  view.findInPage → openFindBar in the global handler map.

- apps/desktop/src/app/contrib/wiring.tsx: mounts <FindBar />
  alongside the other global overlays (CommandPalette,
  SessionSwitcher, etc.).

- apps/desktop/src/i18n/{en,zh}.ts: labels
  'view.findInPage' for the keybinds panel.

Closes #46169
2026-07-26 17:48:05 -07:00
David Metcalfe
7bbb063c71 feat(desktop): port find-in-page bridge to TypeScript Electron
The original PR targeted the CJS Electron files
(apps/desktop/electron/main.cjs and preload.cjs), but commit
39d09453f "feat(desktop): ts-ify everything" renamed them to
main.ts and preload.ts on current main. The PR's diff therefore
targeted files that no longer exist on main.

Brings the bridge forward to the current TypeScript Electron
files and extracts the IPC bridge helpers into a focused
pure-helpers module:

- apps/desktop/electron/find-in-page.ts (new):
  - performFind(webContents, query, options) — wraps
    webContents.findInPage with default-coercing options.
  - stopFind(webContents, action) — clears highlights.
  - formatFoundInPage(result) — pure projection of
    Electron's FoundInPageResult onto the wire payload shape
    ({ activeMatchOrdinal, count }).
  - installFoundInPageForwarder(webContents) — wires a
    sender-scoped 'found-in-page' forwarder; returns an
    uninstall function. Returns a no-op uninstall for null
    or destroyed webContents so callers don't need guards.

- apps/desktop/electron/main.ts:
  - ipcMain.handle('hermes:find-in-page', event => ...)
    resolves the requesting window via
    BrowserWindow.fromWebContents(event.sender) and routes
    the search to THAT window, not the global primary. This
    fixes a multi-window bug where Cmd+F pressed in a
    secondary session window (one per chat, spawned via
    hermes🪟openSession) searched the primary window
    instead of the focused surface.
  - ipcMain.handle('hermes:stop-find-in-page', event => ...)
    routes stopFind through the requesting window for
    multi-window correctness.
  - A per-sender lazy forwarder registry
    (foundInPageForwarders: Map<webContentsId, () => void>)
    installs installFoundInPageForwarder on first
    findInPage call, scoped to the sender's webContents.
    Cleans up automatically via webContents.once('destroyed',
    ...). The forwarder sends results back to the SAME
    renderer that initiated the search, never the global
    primary — so a secondary session window's Cmd+F shows
    matches from THAT window and the match counter reports
    matches from THAT window's DOM.

- apps/desktop/electron/preload.ts:
  - hermesDesktop.findInPage(query, options) — invokes
    the IPC handler.
  - hermesDesktop.stopFindInPage() — invokes the IPC
    handler.
  - hermesDesktop.onFoundInPage(callback) — subscribes to
    'hermes:found-in-page' results from the sender;
    returns an unsubscribe function so the FindBar can
    clean up on unmount.

- apps/desktop/src/global.d.ts:
  - Three new hermesDesktop method declarations:
    findInPage, stopFindInPage, onFoundInPage. The new
    forwarder install registers a 'found-in-page' listener
    bound to the sender's webContents and emits
    'hermes:found-in-page' results back to the sender.

The multi-window fix is part of the same port — the old
PR's behavior (Cmd+F in a secondary session window searched
the global primary) was a bug present in the CJS files,
not a design constraint we wanted to preserve. The new
helper module uses event.sender by design, so the
multi-window correctness lands with the TS port.

Fixes #46169
2026-07-26 17:48:05 -07:00
teknium1
9ca33680ea fix(cli): report unknown line deltas for content-free diffs instead of +0 -0
Found by E2E-rendering the collector against realistic tool payloads.
2026-07-26 17:47:51 -07:00
teknium1
ce997f9e62 feat(cli): per-turn summary line and live token flow in the spinner
Ports Claude Code's post-turn accounting (Edited N files +X -Y · Worked for Ns). Display-only, quiet-mode aware, config-gated.
2026-07-26 17:47:51 -07:00
teknium1
e769560c76 feat(cli): show active /goal segment in the TUI status bar
Append a "⊙ goal 3/20" segment (turns used / turn budget) to the CLI
status bar whenever a standing /goal is active. Mirrors the desktop
composer goal indicator: active-goal-only — paused/done goals stay out
of the bar since they already print their own glyph lines in-thread.

- Snapshot: goal_active / goal_turns_used / goal_max_turns from the
  cached GoalManager (in-memory attribute read, no DB hit per repaint).
- Rendered in all three width tiers of both _build_status_bar_text and
  _get_status_bar_fragments, and it respects the /statusbar toggle for
  free (the toggle gates _get_status_bar_fragments as a whole).
- Tests: segment composition, active-only contract, all width tiers.

Status-bar goal indicator concept from #43020.

Co-authored-by: Akshan Krithick <akshankrithick305@gmail.com>

Assisted-by: Claude Fable 5 via Hermes Agent
2026-07-26 17:47:38 -07:00
teknium1
e5d21e87cb fix(desktop): hydrate goal indicator on /goal set and controls, full locale copy
Follow-ups on the salvaged goal-status display (#63527):

- Seed the goal store from the /goal dispatch notice ("⊙ Goal set …") and
  from /goal status|pause|resume|clear exec output in slash.ts. The backend
  only emits status.update kind:"goal" after the first turn's post-turn
  judge, so without this the indicator stayed empty while the kickoff turn
  ran (sweeper review finding on #63527).
- Add the missing ja / zh-hant statusStack goal copy — desktop ships four
  locales, not two (sweeper review finding on #55651).
- Add a component-level vitest for the composer goal indicator rendering
  from store states: none / active / paused / detail line / other-session.

Co-authored-by: HaisamAbbas <95044189+HaisamAbbas@users.noreply.github.com>

Assisted-by: Claude Fable 5 via Hermes Agent
2026-07-26 17:47:38 -07:00
wjj1872744570-source
bb98595694 Add desktop goal status display 2026-07-26 17:47:38 -07:00
teknium1
24c3c27ba8 feat(cli): hermes import-agent — import Claude Code and Codex CLI setups
Maps CLAUDE.md/AGENTS.md, permission allowlists, MCP servers, skills, and memories into their Hermes equivalents. Follows the openclaw migration pattern. Inspired by ChatGPT Work import-from-another-agent onboarding.
2026-07-26 17:47:07 -07:00
teknium1
62e8842d07 fix(tui): emit multi_select hint only when true — single-select clarify payloads keep the pre-existing protocol shape 2026-07-26 17:46:55 -07:00
teknium1
d778732cc8 chore(contributors): map ghislain.lemeur@gmail.com -> gigi206 2026-07-26 17:46:55 -07:00
teknium1
10b7ab5cb6 feat(clarify): extend multi-select to gateway text fallback and TUI bridge
Cross-surface coverage #23768 missed (per review feedback):

- tools/clarify_gateway.py: _ClarifyEntry carries a multi_select flag
  (register() accepts it; signature() exposes it to adapters).
  _coerce_text_response now parses multi-select replies — comma- or
  space-separated numbers ('1,3' / '1 3'), exact labels, dedup — into a
  JSON array string that _parse_multi_select_response decodes into a
  list. Out-of-range/unknown tokens reject the reply (native button UI)
  or fall back to custom text (awaiting_text/'Other' mode).
- gateway/run.py: _clarify_callback_sync accepts multi_select and
  registers it on the pending entry.
- gateway/platforms/base.py: default numbered-list text fallback tells
  the user multiple selections are allowed and how to reply.
- tui_gateway/server.py: clarify_callback passes multi_select through
  the clarify.request payload as a hint; renderers without checkbox
  support ignore the field and remain single-select-compatible.
- tests: 13 new gateway tests (flag storage, comma/space/single-number
  parsing, label matching, out-of-range rejection, dedup, end-to-end
  resolve, single-select regressions).
2026-07-26 17:46:55 -07:00
teknium1
0b670f0539 fix(clarify): route multi_select through current dispatch paths and harden callback detection
Follow-ups to the salvaged #23768 commit, which targeted a pre-79559214
codebase:

- agent/tool_executor.py + agent/agent_runtime_helpers.py: pass
  multi_select at both current clarify dispatch points (the PR's
  run_agent.py edits landed on dead code paths).
- tools/clarify_tool.py: replace the broad TypeError-retry in
  _invoke_callback with inspect.signature detection, so a compatible
  callback that raises TypeError internally is not invoked twice
  (addresses hermes-sweeper review feedback on #23768).
- tests: cover single-invocation on internal TypeError, legacy 2-arg
  callbacks, **kwargs callbacks, and registry handler multi_select
  pass-through (schema arg → handler → callback).
2026-07-26 17:46:55 -07:00
Ghislain LE MEUR
3e2f91f6b3 feat(clarify): add multi-select (checkbox) support to clarify tool
Adds a `multi_select` boolean parameter enabling checkbox-style
multi-choice questions (Space to toggle, Enter to confirm).
Backward compatible — defaults to single-select when omitted.

- tools/clarify_tool.py: schema + handler + _parse_multi_select_response
- run_agent.py: both dispatch points pass multi_select
- cli.py: checkbox UI, key bindings, rendering, edge cases
- hermes_cli/callbacks.py: TUI fallback callback
- hermes_cli/oneshot.py: oneshot multi-select message
- tests/tools/test_clarify_tool.py: 12 new tests (35 total)
2026-07-26 17:46:55 -07:00
teknium1
bd1db5460a feat(approvals): operator-customizable smart-approval policy via approvals.smart_policy
Inspired by ChatGPT Work auto-review guardian policy customization.
2026-07-26 17:46:42 -07:00
teknium1
95b7ea5e5d feat(cli): /init — generate or update AGENTS.md from a project scan
Cross-surface slash command (CLI, gateway, TUI) following the /learn prompt-injection pattern. Port of Codex /init.
2026-07-26 17:46:29 -07:00
Brooklyn Nicholson
2109a1875e fix(desktop): stop dropping the prompt a mid-turn redirect corrected
redirectPrompt inserts its correction as a second user row just before the
live reply, so one turn can own a contiguous run of user rows. Three recovery
paths each assumed a turn has exactly one, and all three kept the correction
and discarded the prompt that started the turn:

- recoverableTail walked back to the nearest user row, so the crash journal
  never stored the original.
- preserveLocalPendingTurnMessages kept only the newest optimistic user row.
  Widened to the contiguous run — rows separated by an assistant reply are
  still dropped, which is the stale-post-compression case that rule exists for.
- appendLiveSessionProjection had no way to render corrections; it now projects
  them after the prompt, deduped against the transcript's latest user run.

Losing a row also shifted every later role:ordinal pairing in the reconcile,
which is why the thread looked like it compacted rather than just missing one
bubble. Reproducible on a reconnect and on a dev hot update, which remounts the
session cache while the gateway socket survives.
2026-07-26 19:44:42 -05:00
Brooklyn Nicholson
2dd4cbbe61 fix(tui_gateway): keep the original prompt when a redirect corrects a turn
An accepted mid-turn redirect wrote its correction over inflight_turn["user"].
That field is the only user text session.resume can replay, so the prompt that
started the turn was gone the moment the user typed again while it ran. On the
next resume the client rebuilt the thread without it.

Record corrections in their own list instead, alongside the prompt. Renamed
_replace_inflight_user to _record_inflight_correction now that it appends.
_start_inflight_turn rebuilds the dict wholesale, so corrections cannot leak
into a later turn.
2026-07-26 19:43:23 -05:00
Brooklyn Nicholson
0f7492f43a refactor(sidebar): drop session counts and the COUNT(*) that fed them
The sidebar labelled sections and workspace lanes `loaded/total`, which
read as a progress bar people expected to fill up rather than a count of
loaded rows. Pricing that label cost a COUNT(*) per profile database on
every sidebar refresh, purely so the numerator and denominator could
differ.

Pagination only needs to know whether another page exists, and that comes
free from the rows the query already returned: a window that comes back
full means more remain on disk. Sections now show the loaded count alone,
and the backend reports per-profile `profiles_truncated` flags in place of
`total` / `profile_totals`.
2026-07-26 19:36:08 -05:00
teknium1
40dc36a842 fix(deps): converge huggingface-hub on one exact version (1.24.0) across lock and lazy pin
hermes update's lazy-refresh pass re-asserts LAZY_DEPS pins whenever the
package is present (active_features() is presence-based). The
tool.trace_upload pin huggingface-hub==1.2.3 sat below transformers'
>=1.5.0,<2 requirement, so every update force-downgraded the shared
package and broke Hindsight local embeddings on daemon startup (#60783).

Keep the exact-pin security posture — no ranges — but move the pin to
1.24.0 (current) and bump uv.lock in lockstep (uv lock --upgrade-package
huggingface-hub: hub 1.4.1->1.24.0, hf-xet 1.3.1->1.5.2, click
8.3.1->8.4.2, drops typer-slim), so the entire tree converges on ONE hub
version. The refresh pass now reports 'current' with zero churn.

Invariant tests (not snapshots): the lazy pin must equal the uv.lock
resolved version, and must sit inside transformers' accepted window.
HfApi surface used by trace upload (whoami/create_repo/upload_file)
verified present with identical kwargs on 1.24.0 in a live venv.
2026-07-26 17:24:31 -07:00
izumi0uu
35a9b3a7c2 fix(docker): ship a WAL-reset-safe SQLite runtime
Compile and checksum-pin SQLite 3.53.4 in the published image, preserve Hermes' required SQLite features, and assert the final Python linkage plus FTS5 trigram behavior during image builds.\n\nMake doctor remediation install-aware so Docker users pull and recreate every Hermes container instead of running the inapplicable git updater.\n\nFixes #70480
2026-07-26 17:20:31 -07:00
teknium1
c476ad35ae fix(update): refresh stale managed-uv catalog so SQLite runtime repair can succeed
The managed uv is installed with UV_UNMANAGED_INSTALL, which disables
'uv self update' by design — the swallowed failure left its embedded
python-build-standalone catalog frozen at bootstrap age forever.
python-build-standalone re-releases existing patch versions with fixed
SQLite (3.11.15 was re-cut with 3.53.1), so a stale catalog resolves
the same version number to the OLD vulnerable build, the probe rejects
it, and the patch-retry loop cannot recover because the fixed build
carries no newer number to try. Result: 'hermes update' printed a
guaranteed-failure provisioning warning on every run (issue #72093).

- When provisioning fails, re-bootstrap the Hermes-managed uv binary
  via the official installer (the only supported refresh for unmanaged
  installs) and retry provisioning once — only when the binary version
  actually changed, so no wasted download cycles.
- Never touch a caller-supplied uv outside the managed path.
- Soften the failure report from alarming ⚠ to informational ℹ and say
  why it is safe to wait: the WAL gate keeps databases out of WAL on
  vulnerable builds, and the next update retries.

Verified: 56 unit tests green; sabotage run (retry block removed) fails
the 3 new retry tests; live E2E replaced a fake managed uv via the real
astral installer and the refreshed binary resolved the 3.11 catalog.

Fixes #72093
2026-07-26 17:20:27 -07:00
brooklyn!
e2fbd0dcd7
Merge pull request #72303 from NousResearch/bb/desktop-session-status
fix(desktop): stop sidebar sessions from lying about whether they're running
2026-07-26 19:19:22 -05:00
Kevin Haddock
a75ec9278c fix(model): track explicit models: declarations in section 3 so a singular default_model doesn't suppress live discovery
A providers: entry with only a default_model/model (no explicit models:
list) is un-narrowed — the singular field is just the active selection.
Section 3 derived has_explicit_models from the merged models list, so
the lone default_model entry counted as an explicit catalog and
suppressed the /v1/models probe for no-key endpoints, leaving a
one-line /model picker menu for local llama.cpp/Ollama/vLLM servers.

Track explicit models: declarations separately at group-build time
(mirrors section 4's declaration-tracking from #40542 / PR #61928) and
gate the probe on that instead.

Salvaged from PR #68984 by @vigilancetech-com (the probe_custom_providers
gate removal in that PR is not taken — the GUI no-probe gate is
intentional).
2026-07-26 17:17:03 -07:00
teknium1
0f554ce19e test(gateway): drop source-reading guard test from #71671 salvage
Source-regex tests are banned (AGENTS.md 'Never read source code in
tests') — keep only the behavioral regression test.
2026-07-26 17:15:38 -07:00
hereicq
12fdaeaf1a test(gateway): trim docstrings 2026-07-26 17:15:38 -07:00
hereicq
3ec5fb076e fix(gateway): survive faulthandler.enable() when sys.stderr is None
faulthandler.enable() writes to sys.stderr by default, and raises
RuntimeError('sys.stderr is None') when the gateway is launched
without an attached console — e.g. via the Windows Startup VBS shim,
pythonw.exe, a detached service, or any parent that redirects stderr
to DEVNULL. Because this happens on the very first line of
GatewayRunner.start(), the whole gateway used to die at startup and
every configured platform adapter (Discord bot, Telegram, Slack, …)
would silently show offline until the user manually re-ran
'hermes gateway run --replace' from a real terminal.

Wrap the call and fall back to a log-file file descriptor
(logs/gateway_faulthandler.log) when stderr is unavailable, so
fatal-error stack dumps still land somewhere useful. If even the
fallback fails we log-and-continue rather than kill the gateway.

Repro traceback (from a real user's gateway-exit-diag.log, launched
via the Startup VBS with stdin_is_tty=false):

    File "gateway/run.py", line 7821, in start
        faulthandler.enable()
    RuntimeError: sys.stderr is None
2026-07-26 17:15:38 -07:00
teknium1
b792bd0529 feat(delegation): structured stall metadata + live per-child status in /agents
Completes #51690 on top of the salvaged #60378 timeout metadata:

- async_delegation: terminal 'stalled' events now carry structured
  stall context (stalled_after_quiet_seconds, stall_threshold_seconds,
  stall_phase idle|in_tool, stall_grace_seconds) on both single and
  batch paths, persisted in the durable row so restart-restored events
  keep it. Mirrors the sync path's timeout_seconds/timed_out_after_
  seconds/timeout_phase from #60378.
- list_async_delegations(): exposes seconds_since_progress and live
  children_activity (per-child api_calls, current_tool,
  seconds_since_activity) sampled from the dispatch's progress_fn
  outside the records lock; private monitor bookkeeping and callables
  never leak.
- /agents (CLI + gateway): background delegations render per-child
  activity rows, quiet-time hints, and the stalling state; gateway
  section is new (previously async delegations were invisible there).
  New locale key gateway.agents.background_delegations in all 17
  catalogs.

Tests: stall-metadata event shape, live-listing projection, gateway
/agents rendering (real registry dispatch, sabotage-verified), sync
timeout metadata fields, non-timeout None contract.
2026-07-26 17:13:52 -07:00
mahdiwafy
8e163852d8 fix(delegate): include explicit timeout metadata in subagent results
Add timeout_seconds, timed_out_after_seconds, and timeout_phase to timeout
results so parent agents and users can distinguish timeouts before the
first LLM call from timeouts after one or more API calls.

Also attach diagnostic_path to the N>0 API-call timeout error message,
matching the existing zero-API-call timeout path.

Addresses part of #51690 and #17308.
2026-07-26 17:13:52 -07:00
brooklyn!
2b38d5ad59
Merge pull request #72308 from NousResearch/bb/tool-group-bounding
fix(desktop): stop reads and edits vetoing tool-call grouping
2026-07-26 19:13:22 -05:00
Brooklyn Nicholson
19d84d1071 test(desktop): pin the sidebar spinner's liveness contract
Investigating the missing-spinner report turned up no defect in the seeding
path: the active_list poll already lights a row for a turn the renderer never
saw start, holds it across polls, and follows a recycled runtime id onto its
new stored session. Pin all three so the reap change can't silently regress
turn-start while fixing turn-end.

Two boundaries worth naming rather than rediscovering:

- `starting` is deliberately NOT working. It means agent_build_started without
  agent_ready, and _start_agent_build runs on any incidental RPC that needs the
  agent — not just a prompt — so treating it as a turn would spin the row on
  merely opening a session.
- $workingSessionIds is keyed by STORED id and drops entries whose
  storedSessionId is null, while message.start flips busy without carrying one.
  A runtime that was never seeded with a stored id therefore goes busy
  invisibly. That is the remaining path by which a spinner can go missing.
2026-07-26 19:02:46 -05:00
teknium1
136f8dab67 refactor(gateway): promote autonomous silence matcher to shared response_filters helper
Follow-up to the salvaged #71756: instead of webhook importing cron's
private _is_cron_silence_response, the loose autonomous-lane matcher now
lives in gateway/response_filters.py as is_autonomous_silence_response,
sharing LIVE_GATEWAY_SILENT_MARKERS with the interactive exact-marker
rule so the marker sets can never drift. Cron and webhook both delegate
to it. Interactive gateway behavior unchanged.
2026-07-26 16:55:42 -07:00