Commit Graph

242 Commits

Author SHA1 Message Date
Michael Lam 0bd65ef0bf fix: preserve CLI session tool metadata 2026-05-07 02:47:19 +00:00
Frank Song 91f99d8194 fix(oauth): serialize Anthropic env fallback reads 2026-05-07 02:47:19 +00:00
nesquena-hermes f77a44fce2 feat(ux): three high-leverage context-menu essentials from #1764
Issue #1764 asked for a much larger surface (Reveal + Copy-path on
every UI surface that references a file path, plus Rename in session
menus). Per Nathan's curation we ship only the three highest-leverage
pieces in this PR — they cover the three concrete user-visible
frictions Cygnus reported, and leave the broader sweep for follow-up.

## 1. Copy file path in workspace tree right-click menu

The tree's right-click already had Rename and Reveal in File Manager.
Reveal is slow when the user just wants the path string for a
terminal/editor — and there was no Copy-path action anywhere.

Added "Copy file path" between Reveal and Delete. It POSTs to a new
`/api/file/path` endpoint that resolves the relative tree-rooted path
into the absolute on-disk path (the frontend can't compute it because
only the server knows the workspace root) and writes the result to
the OS clipboard via `navigator.clipboard.writeText()`. Falls back to
the legacy execCommand pattern on browsers where the modern Clipboard
API is gated.

The new endpoint deliberately does NOT require the target to exist:
copy-path on a recently-deleted file is still useful (paste into a
terminal to investigate). `safe_resolve` continues to gate path
traversal — the test suite pins this with a `../../../../../etc/passwd`
attempt that 400s.

## 2. Rename in session three-dot menu

Cygnus's specific ask: double-click rename in the sidebar is timing-
sensitive — the first click frequently registers as "open the chat"
before the second click arrives, so users open the conversation when
they meant to rename it. Putting Rename in the menu eliminates the
timing entirely.

Added Rename as the FIRST item in `_openSessionActionMenu` (above
Pin). It reuses the existing `startRename` closure attached to each
session row — no duplicated state, no second API call out of band
with the double-click path. Mechanism: the row builder now stores
`el._startRename = startRename` and `el.dataset.sid = s.session_id`,
so the menu can find the row by data-sid and call its closure
directly. This keeps all the `_renamingSid`/`oldTitle`/`applyTitle`
bookkeeping single-sourced.

Read-only imported sessions skip the menu item via the same
`_isReadOnlySession` gate the closure already uses.

## 3. Reveal-failed toast includes the resolved server-side path

Cygnus posted a screenshot of a "Failed to reveal: not found" toast
that dropped the path entirely. Without it the user can't tell which
file the system expected — useful when a stale session row still
references a deleted file.

Server-side fix in `_handle_file_reveal`: instead of returning
`bad(handler, "File not found", 404)`, return
`bad(handler, f"File not found: {target}", 404)` where target is the
resolved absolute path. Frontend toast also defends against err with
no .message: `(err.message||err)` instead of `err.message` alone.

Verified live: a missing-file reveal now produces:

    Failed to reveal: File not found: /home/hermes/workspace/missing-xyz.txt

Cygnus's exact diagnostic-friction is gone.

## Tests

* tests/test_1764_context_menu_essentials.py (new)
  - 13 source-level pinning tests
  - 6 live HTTP behaviour tests against the conftest test server

* tests/test_1466_sidebar_cancel_clarify.py
  - Two assertion-window bumps (3200→4400, 3600→4800) to accommodate
    the new Rename action prepended to _openSessionActionMenu. The
    test relied on a fixed-byte-window function-body slice — comments
    added explaining why the bumps were needed.

* All 9 locales got translations for the 5 new keys
  (copy_file_path, path_copied, path_copy_failed, session_rename,
  session_rename_desc) — locale parity tests pass.

## Verification

Full pytest suite: 4671 passed, 2 skipped, 3 xpassed (matches
pre-change baseline).

Live browser verification on port 8789:
- Right-click .git folder in workspace tree → menu shows
  Rename / Reveal in File Manager / Copy file path / Delete (red).
- Click Copy file path → clipboard gets "/home/hermes/workspace/.git",
  toast confirms "File path copied to clipboard".
- Open session three-dot menu → Rename conversation appears first
  with pencil icon, followed by Pin / Move / Archive / Duplicate /
  Delete in the same order as before.
- Trigger reveal on a non-existent file → toast reads
  "Failed to reveal: File not found: /home/hermes/workspace/<filename>".
  The resolved server-side path is now visible in the failure.

Refs nesquena/hermes-webui#1764.
2026-05-07 01:39:52 +00:00
Michael Lam 1fc8e83c90 fix: use spawn for manual cron subprocesses 2026-05-07 01:39:51 +00:00
skspade 7193cee152 fix: tri-state gateway status — distinguish not-configured from not-running
- Backend: return `configured` field alongside `running`. When
  alive=None (no gateway metadata), configured=false with fallback to
  identity_map heuristic.
- Frontend: amber "Gateway not configured" when configured=false,
  red "Gateway not running" only when configured but process is down,
  green "Running" when both true.
- Replace dead try/except fallback with explicit tri-state check on
  health["alive"].
- Add regression test for last_active guard when alive=true and
  identity_map is empty.

All 87 gateway-related tests pass.
2026-05-06 22:01:36 +00:00
skspade eab39f14db fix: gateway status card shows 'not running' when no platforms connected
Use agent_health.build_agent_health_payload() as the authoritative
running signal instead of bool(identity_map). An empty identity_map
means zero connected messaging platforms, not that the gateway is down.

Falls back to identity_map heuristic when agent_health module is unavailable
(e.g. WebUI-only deployments).
2026-05-06 22:01:35 +00:00
Michael Lam dcc8268c92 fix: drain cron subprocess results before join 2026-05-06 18:11:14 +00:00
Michael Lam b9bf00efe1 fix: shorten cron profile lock for manual runs 2026-05-06 18:11:14 +00:00
nesquena-hermes ff0d25fd0e fix(workspace): strip surrounding quotes from Add Space path input
macOS Finder's 'Copy as Pathname' (Cmd+Option+C) wraps paths in single
quotes by default — '/Users/x/Documents/foo' — and users routinely paste
those quoted strings into the Add Space input expecting them to work.
Other shells and OS file managers do similar things with double quotes.

Today the path is taken via .strip() only, so the literal quote
characters become part of the resolved Path and the validator rejects
the result as 'not a directory'. cygnus reported this on Discord
(2026-05-01) — she had to manually un-quote her paths to register a
new Space.

Fix:
  - New api.workspace._strip_surrounding_quotes() helper. Removes only
    the outermost paired single or double quotes; preserves unpaired or
    mismatched quotes (a path may legitimately contain a literal quote).
  - validate_workspace_to_add() calls it before resolution so every
    code path that registers a workspace benefits, not just the HTTP
    route.
  - _handle_workspace_add() also calls it at the route entry so the
    blocked-system-path check and the duplicate-detection check both
    see the cleaned form.

14 regression tests pin the behavior matrix:
  - Unwrapped path unchanged
  - Single quotes stripped
  - Double quotes stripped
  - Whitespace outside quotes handled (trim-then-strip)
  - Only outermost pair removed (internal quotes preserved)
  - Unpaired / mismatched quotes preserved
  - Empty string + just-a-pair edge cases
  - Validate_workspace_to_add accepts quoted form for existing dir

4610 tests pass (+14 from this PR), 0 regressions, ~2:27 full suite.

Reported by Cygnus on Discord, May 1 2026.
2026-05-06 17:38:11 +00:00
nesquena-hermes ec403fa3cf fix(routes): persist openai-codex provider unconditionally on stale-session repair (Opus stage-303 follow-up)
Opus advisor on stage-303 (#1738 verification Q4) flagged that the
catalog-coverage branch produces a redundant repair-write per chat-start
when the active Codex default is itself slash-prefixed: the repair sets
`provider_context = None`, the next chat-start hits the same branch
because `requested_provider is None` again, and the repair fires repeatedly.

In practice Codex `default_model` is always a bare `gpt-...` ID from the
Codex catalog, so this is theoretical. But once we've decided this session
belongs to Codex, we should persist that decision. Drop the conditional
catalog-coverage check and unconditionally attach `raw_active_provider`
("openai-codex") on this repair path. The shape is now stable across
resolutions.

Absorb-in-release per Opus stage-303 verdict — small, defensive, ≤10 LOC.
2026-05-06 15:18:34 +00:00
Michael Lam 3e2a945501 fix: repair stale OpenAI session models for Codex 2026-05-06 14:53:40 +00:00
starship-s 74eb55d986 fix(profile): preserve context when starting chats 2026-05-06 06:27:00 +00:00
Nathan Esquenazi b6567addb1 Stage 303: PR #1719 2026-05-05 21:58:21 +00:00
Michael Lam 2c5acb9725 feat: show active elapsed timer in compact activity 2026-05-05 13:42:47 -07:00
ai-ag2026 8b34a79f02 fix: preserve imported session lineage visibility 2026-05-05 22:32:19 +02:00
Michael Lam fdeac578da feat: add VPS resource health panel 2026-05-05 17:30:56 +00:00
test 32f37d3d78 Stage 300: PR #1676 — Add Hermes agent heartbeat alert by @Michaelyklam 2026-05-05 02:27:24 +00:00
Michael Lam 22df075b8a feat: add active provider quota status 2026-05-05 02:26:52 +00:00
Michael Lam 960e45f77f feat: add agent heartbeat alert 2026-05-05 02:25:06 +00:00
Nathan Esquenazi e2748fe961 Apply Opus pre-release SHOULD-FIX (absorbed in stage-299)
Per Opus advisor on stage-299:

1. Bounded WIKI_PATH walk + forbidden-root guard (api/routes.py)
   - _LLM_WIKI_MAX_FILES = 10000 caps rglob iteration (prevents hangs on
     symlink loops or pathologically-large trees)
   - _LLM_WIKI_FORBIDDEN_ROOTS blocklist refuses '/' '/etc' '/usr' '/var'
     '/opt' '/sys' '/proc' even if WIKI_PATH is misconfigured to point
     at them
   - Self-DoS prevention: /api/wiki/status fires on every Insights tab
     open via Promise.all, and unbounded rglob would block the endpoint

2. URL-scheme guard for docs_url interpolation (static/panels.js)
   - rawDocsUrl is regex-validated against /^https?:\/\//i before being
     interpolated into the <a href=> attribute
   - esc() HTML-escapes but doesn't validate URL scheme; docs_url is
     server-controlled today but the contributor scaffolded it for
     potential config-driven use, so future-proof against javascript:
     scheme XSS

6 regression tests in tests/test_stage299_opus_fixes.py pin both fixes.
2026-05-05 02:15:25 +00:00
test 136d858963 Stage 299: PR #1587 — Filter low-value CLI agent sessions by @franksong2702 2026-05-05 01:54:08 +00:00
test df8ee6a8ad Stage 299: PR #1662 — feat(logs): add Logs tab MVP by @Michaelyklam 2026-05-05 01:53:56 +00:00
Frank Song 79d0762d8c Filter low-value CLI agent sessions 2026-05-05 01:52:42 +00:00
Michael Lam af1c628292 feat: add logs tab MVP 2026-05-05 01:51:05 +00:00
Michael Lam 2684d6fa98 feat: add LLM Wiki status panel 2026-05-05 01:48:32 +00:00
test 3699e83c43 Stage 298: PR #1677 — feat: link official Hermes dashboard by @Michaelyklam 2026-05-05 01:29:49 +00:00
Michael Lam b0953b6a7f feat: link official Hermes dashboard 2026-05-05 01:23:55 +00:00
Michael Lam e0e991126f feat: add searchable MCP tool inventory 2026-05-05 01:20:32 +00:00
test 2ec18b728a Stage 298: PR #1670 — feat: add MCP server visibility panel by @Michaelyklam 2026-05-05 01:18:35 +00:00
test 8c93b995ef Stage 298: PR #1678 — Add Claude Code session imports by @Michaelyklam 2026-05-05 01:18:35 +00:00
test def1507828 Stage 298: PR #1674 — feat(tasks): add scheduled job profile selector by @Michaelyklam 2026-05-05 01:18:35 +00:00
test dfb3798470 Stage 298: PR #1663 — feat: add plugins visibility panel by @Michaelyklam 2026-05-05 01:18:35 +00:00
Michael Lam 399326f923 feat: add MCP server visibility panel 2026-05-05 01:18:34 +00:00
Michael Lam e54a0470f0 Add Claude Code session imports 2026-05-05 01:18:34 +00:00
Michael Lam 3f3092a84e feat: add scheduled job profile selector 2026-05-05 01:18:34 +00:00
Michael Lam 60ed948f42 feat: add plugins visibility panel 2026-05-05 01:18:33 +00:00
Michael Lam 66755b7fb1 feat: add insights token trends 2026-05-05 01:12:08 +00:00
Manfred 711e33e7db feat: harden Kanban review feedback
- add canonical PATCH and DELETE routing for Kanban writes
- fix task detail log rendering and add close/back affordance
- improve timestamps, event summaries, stats HUD, and mobile layout
- cover route and detail behavior with targeted tests
2026-05-04 22:56:43 +00:00
Manfred 5093e01640 feat: add Kanban write semantics MVP 2026-05-04 22:56:43 +00:00
Manfred eeb5dc545d feat: add read-only Kanban API bridge 2026-05-04 22:56:42 +00:00
Hermes Agent 3005bfc491 chore(release): stamp v0.50.297 — 3-PR batch + Opus pass + 2 follow-ups absorbed
Constituent PRs:
  #1659 by @bergeouss — Docker readonly false-positive (closes #1658, fixes v0.50.295 regression)
  #1653 by @nesquena — OAuth cancel race fix (follow-up to v0.50.296 #1652)
  #1657 by @Michaelyklam — health diagnostics + watchdog hardening (refs #1458 Bug #3)

Opus advisor SHIP verdict on stage-297. Two follow-ups absorbed in-release:
- _deep_health_checks(stream_check=...) reuses pre-computed lock probe
- _handle_request_noblock docstring documents single-thread safety

PR #1656 closed as superseded by #1657 (same author, both target #1458,
#1657 is functional superset).

4284 → 4288 tests passing (+4).
2026-05-04 22:50:57 +00:00
Michael Lam ca135c2015 fix: harden persistent WebUI health checks 2026-05-04 15:30:37 -07:00
test c07d821586 Stage 296: PR #1650 — Codex OAuth onboarding flow (refs #1362) by @Michaelyklam 2026-05-04 21:26:52 +00:00
Michael Lam 259c5c4afb feat: add Codex OAuth onboarding flow 2026-05-04 14:07:16 -07:00
Michael Lam 876a670387 feat: add session save mode config 2026-05-04 14:05:49 -07:00
bergeouss 324aeaaded fix: macOS auto-scroll momentum race (#1360) + custom:* provider model list (#1619)
#1360 — On macOS WKWebView, trackpad momentum scrolling fires scroll
events that interleave with the _programmaticScroll setTimeout(0) guard.
A mid-momentum scroll event either gets swallowed (_programmaticScroll
still true) or falsely reports nearBottom (momentum hasn't settled),
keeping _scrollPinned=true and snapping the viewport back down.

Fix: rAF-debounce the scroll listener so the nearBottom check runs at
the next paint frame when the browser's scroll position has settled.
Added a hysteresis counter requiring 2 consecutive near-bottom samples
before re-pinning, preventing accidental re-pin during deceleration.

#1619 — When a custom:* provider (e.g. custom:relay via custom_providers)
has models that overlap with auto-detected models from base_url /v1/models,
the dedup logic at config.py:2263 skipped them all. The named custom
group ended up empty, and the continue at line 2334 silently discarded
the auto-detected models. Result: only the default model appeared.

Fix 1 (config.py): When custom:* named group has 0 models after dedup,
fall back to auto_detected_models_by_provider instead of dropping them.

Fix 2 (routes.py): Extended /api/models/live fallback to handle
custom:* slugs (not just bare "custom") for both custom_providers
config lookup and base_url live fetch.
2026-05-04 18:23:04 +00:00
nesquena-hermes bea57beba9 fix(streaming): SSE heartbeat alignment, repair grace period, local-server model id preservation (#1623, #1624, #1625)
Closes #1623 — Lower SSE app heartbeat from 30s to 5s at every long-lived
handler (main agent, terminal, gateway-watcher, approval-poller, clarify-poller).
Kernel TCP keepalive declares peer dead at 25s worst-case (10s KEEPIDLE +
5s KEEPINTVL * 3 KEEPCNT, added v0.50.289 #1581). 30s app heartbeat let the
kernel tear sockets down on flaky networks before the app sent its first
keepalive byte — drops at ~10s during long thinking phases. New named
constant _SSE_HEARTBEAT_INTERVAL_SECONDS=5; regression test pins the
inequality (app_heartbeat * 2 <= kernel_window) so future tuning can't
re-introduce the misalignment.

Closes #1624 — Add 30s grace period to _repair_stale_pending() trigger.
Without it, any narrow race between the streaming thread clearing
pending_user_message and STREAMS.pop(stream_id) produces a false-positive
'Previous turn did not complete.' marker on a turn that finished correctly
(reproducible after every command-approval turn). Defense-in-depth, not
the root-cause fix — the actual streaming-thread leak path is tracked
separately. Falsy pending_started_at (legacy sidecars) treated as
'old enough' so legitimate legacy-data recovery still works. Plus
logger.warning telemetry on every legitimate repair so the next batch of
user reports tells us whether the underlying race still fires.

Closes #1625 — Local model servers (LM Studio, Ollama, llama.cpp, vLLM,
TabbyAPI, koboldcpp, textgen-webui) now keep the full HuggingFace-style
model id (e.g. 'qwen/qwen3.6-27b' instead of stripped 'qwen3.6-27b'). New
_LOCAL_SERVER_PROVIDERS set + _base_url_points_at_local_server() loopback/
RFC1918 heuristic — either signal triggers no-strip. Backward compat
preserved for OpenAI-compatible proxies on public hosts (LiteLLM at
litellm.example.com still strips openai/gpt-5.4 -> gpt-5.4). Updated the
existing #230/#433 test to reflect that #1625 supersedes the strip-on-custom
rule for loopback hosts (see api/config.py and test_model_resolver.py
docstring update). Reported by @akarichan8231 in Discord on 2026-05-04.

42 regression tests across:
  tests/test_issue1623_sse_heartbeat_alignment.py (3)
  tests/test_issue1624_repair_stale_pending_grace.py (9)
  tests/test_issue1625_local_server_model_id_preservation.py (30)

4142 -> 4184 passing. 0 regressions.
2026-05-04 16:49:43 +00:00
test 838645fd50 Stage 293: PR #1629 — profile isolation trio (closes #1611, #1612, #1614) by @nesquena-hermes — APPROVED 2026-05-04 16:21:29 +00:00
nesquena-hermes 6bc0f9c4d5 Apply Opus pre-release SHOULD-FIX + NITs (in-PR per release policy)
SHOULD-FIX #1 (renamed-root client cross-alias): drop strict-equality client
filter at static/sessions.js:1853. Server-side _profiles_match cross-aliases
'default'-tagged rows to a renamed root 'kinni'; the strict-equality client
would reject them, dropping every legacy session for renamed-root users. The
server is now solely authoritative for profile scoping.

SHOULD-FIX #2 (messaging-source dedupe ordering): _keep_latest_messaging_session_per_source
now runs AFTER the profile filter at api/routes.py:2078. Before, it ran on
the merged-cross-profile list with profile-blind keys, discarding the older
profile's row across profiles before the scope filter — leaving zero rows for
any messaging identity the active profile shared with another profile.

NIT #3: _projects_migrated flag now set only AFTER successful save_projects.
NIT #4: cleaned dead test code in test_is_root_profile_invalidation_drops_stale.
NIT #5: _create_profile_fallback's clone_from=='default' literal now routes
through _is_root_profile() for parity with the 5 other callsites.

+2 regression tests pin the SHOULD-FIX shapes:
- test_keep_latest_messaging_runs_after_profile_filter (source-string ordering)
- test_static_sessions_js_trusts_server_profile_scoping (no client re-filter)

4173 -> 4175 tests pass. 0 regressions.
2026-05-04 16:17:26 +00:00
nesquena-hermes e8862632ed fix(profiles): scope sessions, projects, and root-profile resolution to active profile (#1611, #1612, #1614)
Closes #1611 — /api/sessions filters by active profile by default; ?all_profiles=1
opt-in for aggregate views; new _profiles_match() helper honours renamed-root
cross-aliasing; static/sessions.js drops the s.is_cli_session bypass; toggle-on
re-fetches with all_profiles=1 instead of slicing client-cached rows.

Closes #1612 — new _is_root_profile() central helper consults list_profiles_api()
for is_default=True matches alongside the legacy 'default' alias. Replaces five
literal-default callsites in api/profiles.py. Memoized with explicit invalidation
hooks at create + delete. Sticky active_profile file write now stores '' for
renamed root, consistent with the legacy empty==root contract.

Closes #1614 — projects carry a profile field stamped at create-time;
/api/projects filters by active profile; /api/projects/{create,rename,delete}
and /api/session/move reject ops on cross-profile projects with 404; new
_PROJECTS_MIGRATION migration in load_projects() back-tags untagged projects
from any session that uses them, fall back to 'default'; ensure_cron_project
keys lookup by (name, profile) so each profile gets its own Cron Jobs project.

31 regression tests (9+11+11) pin the renamed-root resolution, server-side
profile scoping shape, helper invariants, cross-alias matching, migration
behavior, and active-profile guards on every project mutation endpoint.
4148 tests pass.

Reporter: @stefanpieter

Co-authored-by: stefanpieter <noreply@github.com>
2026-05-04 16:03:05 +00:00