Commit Graph

382 Commits

Author SHA1 Message Date
Hermes Bot 3abae9aca7 chore(release): stamp v0.50.267 — 7 contributor PR batch + Opus follow-up
- CHANGELOG.md: v0.50.267 entry detailing #1454/#1474/#1461/#1465/#1467/#1460/#1473
  + Opus advisor SHOULD-FIX trailing-empty guard for _norm_model_id
- ROADMAP.md: bump to v0.50.267, 3776 tests collected
- TESTING.md: bump header + total to 3776
- api/config.py: trailing-empty fallback in _norm_model_id (parts[-1] or s)
- static/ui.js: mirror trailing-empty fallback in _normalizeConfiguredModelKey
- tests/test_norm_model_id_trailing_empty_guard.py: 5 regression tests
2026-05-02 17:03:25 +00:00
happy5318 d6164cdadb Fix _norm_model_id to properly strip provider prefixes
The _norm_model_id function was using split(':', 1)[1] which only removed
the first colon-separated segment, leaving provider names in the normalized
model ID. For example, '@custom:jingdong:GLM-5' became 'jingdong:glm.5'
instead of 'glm.5'.

This caused the default model injection check to fail, resulting in a
duplicate 'Default' group being added to the model list even when the
model already existed with a provider prefix.

Changes:
- Use split(':')[-1] to get the last segment after all colons
- Use split('/')[-1] consistently for slash-separated paths
- Replace local _norm lambda with _norm_model_id function call

Fixes duplicate Default group appearing in model dropdown when using
custom providers with @provider:model ID format.
2026-05-02 13:40:38 +08:00
nesquena-hermes c73f2ff387 v0.50.264 polish followups: i18n parity + assistant-output readability
Closes #1442 (server-side _LOGIN_LOCALE missing ja/pt/ko)
Closes #1443 (promote _isImeEnter helper to 6 other Safari Enter guards)
Closes #1446 (glued-bold-heading lift for LLM thinking-block output)
Closes #1447 (markdown heading visual hierarchy in chat messages)

All four issues were filed by the Opus pre-release advisor on the v0.50.264 batch
or by Cygnus via Discord (relayed by @AvidFuturist, May 1 2026). They share a
common shape — narrow, well-scoped, independent of each other, all adding
regression tests.

== #1442: _LOGIN_LOCALE parity (api/routes.py + static/i18n.js) ==

Added entries for ja/pt/ko to the server-side _LOGIN_LOCALE dict that renders
the localized login page BEFORE the JS i18n bundle loads. With v0.50.264
shipping Japanese as the 8th built-in locale, ja/pt/ko users were seeing the
English login page even with their language preference set.

While auditing static/i18n.js for English leakage, also fixed:
  - ko: 10 user-facing login/sign-out/password keys still in English
  - es: 3 sign-out/auth-disabled keys still in English

Tests: tests/test_login_locale_parity.py (20 tests) — pins both invariants:
  (a) every locale in i18n.js LOCALES has a matching _LOGIN_LOCALE entry
  (b) every locale's login-flow keys (13 of them) are translated, not English

== #1443: window._isImeEnter promotion ==

PR #1441 fixed the Safari IME-composition Enter race in the chat composer
(`#msg`) by widening the guard from `e.isComposing` to a `_isImeEnter(e)`
helper that combines three signals (isComposing || keyCode===229 ||
_imeComposing flag). Six other Enter-input handlers were left on the original
narrow guard and would still drop IME composition Enters on Safari for
Japanese/Chinese/Korean users.

Promoted the helper to `window._isImeEnter` (defined in static/boot.js) and
replaced the `e.isComposing` guards at all six sites:

  - static/sessions.js: session rename, project create, project rename
  - static/ui.js: app dialog (confirm/prompt), message edit, workspace rename

The state-free part of the helper (`isComposing || keyCode===229`) handles
Safari's race for any focused input without needing per-input composition
listeners — only `#msg` keeps the local `_imeComposing` flag.

Tests:
  - tests/test_issue1443_ime_helper_promotion.py (9 tests) — pins each site
    + verifies no raw `e.isComposing` Enter-guards remain in sessions.js/ui.js
  - tests/test_ime_composition.py — alternation regex extended to accept
    the windowed helper form (loosen-test-on-shape-change pattern from
    v0.50.264 reflection notes)

== #1446: glued-bold-heading lift (static/ui.js renderMd + Python mirror) ==

LLMs in thinking/reasoning mode emit "section headers" glued to the end of the
previous paragraph with no whitespace:

    Para 1 text.**Heading to Para 2**

    Para 2 text.**Heading to Para 3**

The renderer correctly produces inline `<strong>` per CommonMark, but it looks
like trailing emphasis on the body text rather than a section break. Cygnus
reported this as "Markdown feedback 2 of 3."

Added a single regex pre-pass in renderMd():

    s.replace(/([.!?])\*\*([^*\n]{1,80})\*\*\n\n/g, '$1\n\n**$2**\n\n')

Constraints chosen to avoid false positives:
  - Trigger only on `[.!?]` IMMEDIATELY before `**` (no space) — almost always
    an LLM-glued heading, not intentional emphasis
  - Inner text ≤80 chars, no `*` or newline (single-line only)
  - Trailing `\n\n` required — preserves "this is **important** to know."
    mid-paragraph emphasis untouched
  - Position: after rawPreStash restore, before fence_stash restore — fenced
    code blocks stay protected (their content is `\x00P` / `\x00F` tokens
    when the lift runs)

Mirrored in tests/test_sprint16.py render_md() so both stay in sync.

Tests: tests/test_issue1446_glued_heading_lift.py (17 tests, 5 of which drive
the actual ui.js renderMd via node) — covers all 3 trigger forms (.!?), all 4
preserve-emphasis cases the issue spec'd, fenced/inline code protection,
chained glued headings, source-level position pin, regex shape pin.

== #1447: markdown heading visual hierarchy (static/style.css) ==

Pre-fix sizes in `.msg-body`:
  h1 18px, h2 16px, h3 14px (= body), h4 13px, h5 12px, h6 11px

So h3 was indistinguishable from body and h4/h5/h6 were SMALLER than body.
Cygnus's report: "Markdown feedback 3 of 3 — Headings seem to be missing
across the board in Hermes. They're there, but all plaintext."

New sizes:
  h1 24px (border-bottom)  h2 20px (border-bottom)  h3 17px  h4 15px
  h5 14px (uppercase, tracked)  h6 13px (uppercase, tracked, muted)

All headings now `font-weight:700` + `color:var(--strong)` for stronger ink.
h5/h6 use uppercase + letter-spacing for "label-style" affordance instead
of being smaller-than-body.

Synced .preview-md (file preview pane) to match exactly so a markdown file
preview and a chat message render identically. Added missing h4/h5/h6 rules
to .preview-md (it only had h1-h3 before).

Updated data-font-size="small"/"large" h1-h6 overrides to scale
proportionally with the new defaults. Hierarchy preserved at all three
font-size settings.

Tests: tests/test_issue1447_heading_hierarchy.py (9 tests) — pins the size
hierarchy, the bottom borders on h1/h2, the uppercase affordance on h5/h6,
the .preview-md sync, and the small/large override scaling.

== Verification ==

  pytest tests/ -q                                  → 3748 passed (+56 new)
  bash ~/WebUI/scripts/run-browser-tests.sh         → 20 + 11 PASS
  bash ~/WebUI/scripts/webui_qa_agent.sh 8789       → 23/23 PASS

Visual confirmation in browser at port 8789:
  - Heading hierarchy clearly visible at all 6 levels
  - Glued-bold lift produces separate paragraphs as designed
  - window._isImeEnter accessible from any module after boot.js
  - Login page renders ja/pt/ko strings correctly (curl -s /login)
2026-05-02 04:19:28 +00:00
nesquena-hermes 4ee9368464 Opus pre-release follow-ups for PR #1445
REQUIRED:
- _fully_unquote_path range(3) -> range(10) — defense-in-depth so quadruple-
  encoded .. is rejected by validator instead of slipping through (not
  exploitable but contract violation)
- docs/EXTENSIONS.md trust-model callout moved to top of file with explicit
  'don't enable in untrusted env / don't point at user-writable dir' guidance

NICE-TO-HAVE (taken since Nathan asked for all fixes big and small):
- URL list cap at _MAX_URL_LIST=32 to avoid pathological rendering
- One-shot WARNING log for rejected URLs (silent drop now visible to admin)
- One-shot WARNING log for URL list truncation
- MIME map: ttf (font/ttf), otf (font/otf), wasm (application/wasm)

5 regression tests in tests/test_pr1445_opus_followups.py pin all invariants.
2026-05-02 03:49:40 +00:00
Ryan Jones 9de61a0b9a feat: add opt-in webui extension hooks 2026-05-02 03:36:54 +00:00
nesquena-hermes 081e600b33 fix: context-window indicator broken on older sessions (#1436)
Fix two-layer bug where `/api/session` returned `context_length=0` for
sessions that pre-date #1318, then the frontend silently fell back to
cumulative `input_tokens` and the 128K JS default, producing nonsense
indicators like "100" capped from "890% used (context exceeded), 1.2M
/ 131.1k tokens used".

Empirical impact: 23 of 75 sessions on dev server rendered >100% before
this fix. #1356 fixed the same symptom on the live SSE path but missed
the GET /api/session load path that older sessions go through.

Two-layer fix:
  1. Backend (api/routes.py:1295-1313) — resolve context_length via
     agent.model_metadata.get_model_context_length() when the persisted
     value is 0. Mirrors api/streaming.py:2333-2342.
  2. Frontend (static/ui.js:1269) — drop the cumulative `input_tokens`
     fallback. When last_prompt_tokens is missing, render "·" + "tokens
     used" (existing !hasPromptTok branch) instead of computing a
     percentage from the cumulative total.

10 regression tests in tests/test_issue1436_context_indicator_load_path.py
covering both layers + the empty-model edge case (avoids the 256K
default-for-unknown-model trap that get_model_context_length('') returns).

Verified live: claude-opus-4-7 session with input_tokens=5,226,479 now
renders "·" + "5.3M tokens used" instead of "100" + "3987% used".

Reported by @AvidFuturist.
Closes #1436.
2026-05-02 01:43:00 +00:00
nesquena-hermes c75ce33280 v0.50.259: Opus pre-release follow-up — close _session_db on LRU eviction + CHANGELOG + 5 regression tests
PR #1421 (SessionDB WAL handle leak fix on cached-agent reuse path) had a
sibling leak at the LRU eviction site that I caught during pre-review:

api/streaming.py SESSION_AGENT_CACHE.popitem(last=False) was discarding
the evicted entry with `evicted_sid, _ = ...`. The agent's _session_db
was dropped on the floor and only released when GC eventually finalized
the agent — which on a long-running server may be never (cyclic refs,
extension types holding C handles, etc.).

Same fix shape as #1421: capture the evicted entry, call
_evicted_agent._session_db.close() explicitly. SessionDB.close() is
idempotent + thread-safe (with self._lock: if self._conn:), so the
double-close-is-benign property still holds.

5 regression tests in test_v050259_sessiondb_fd_leak.py:
- Source-level: cached-agent reuse path closes before replace
- Source-level: LRU eviction path captures + closes evicted agent
- Behavioral: SessionDB.close() is idempotent (3 calls safe)
- Behavioral: cached-agent reuse with mock — close called exactly once
- Behavioral: LRU eviction with mock — only evicted agent's DB closes

Full suite: 3615 passed, 0 failed.

Nathan explicitly authorized 'just go ahead and merge it as a small release'
since the PR is 9 LOC, focused, has Opus pre-release follow-up + tests, and
matches the empirically-confirmed leak shape (73-handle leak at EMFILE).
2026-05-01 22:42:53 +00:00
nesquena-hermes f05893215e Merge PR #1421 from wali-reheman: close previous SessionDB before replacing on cached agent 2026-05-01 22:38:53 +00:00
nesquena-hermes 399f12ac96 v0.50.258: Opus follow-up — fix multi-param redirect-encoding bug + CHANGELOG
PR #1419 (login session TTL + redirect-back + connectivity probe) had a
real bug in the server-side ?next= construction:

quote(path, safe='/:@!$&'()*+,;=') keeps ? and & literal, so:

(a) /api/sessions?limit=50&offset=0 round-trips as /api/sessions?limit=50
    — the inner & terminates the outer next= value and offset=0 leaks as
    a top-level outer query the login page ignores.

(b) An attacker-controlled path with embedded &next=https://evil.com
    injects a second top-level next parameter. Browsers parse first-match
    (benign), Python parse_qs parses last-match (the evil URL) — the
    parser-divergence is a footgun even though _safeNextPath() in login.js
    rejects the actual exploit.

Fix: encode the entire path?query blob with safe='/' so ?, &, = all
percent-encode. The outer next then holds exactly one path-with-query
string the browser auto-decodes once.

6 regression tests in test_v050258_opus_followups.py pin round-trip behavior
across simple paths, single-query, multi-param queries, attacker-injection
neutralization, and the SESSION_TTL=30d constant.

Full suite: 3610 passed, 0 failed.
2026-05-01 21:30:10 +00:00
Wali Reheman 9b987eefb0 fix: close previous SessionDB before replacing on cached agent
SessionDB WAL handles leak when streaming.py creates a new SessionDB
instance per request and replaces the cached agent's _session_db without
closing the old one. Each orphaned connection holds 2 FDs (.db +
.db-wal), causing FD exhaustion and EMFILE crashes after ~73 messages.

Fix: close the previous _session_db before replacing it on cached
agents, mirroring the close-before-replace pattern used elsewhere in the
codebase.
2026-05-01 13:51:21 -07:00
bsgdigital 9c0667d187 fix(auth): extend session TTL to 30 days + redirect back after login 2026-05-01 19:54:47 +00:00
nesquena-hermes c78bcddda6 v0.50.257: CRITICAL Opus finding — fix non-functional per-session toolset override
Opus pre-release advisor caught a 5th issue not covered by my initial
follow-up sweep, this one CRITICAL: PR #1402 #493 per-session toolset
override silently no-op'd every time.

Bug: api/streaming.py:1755 called _session_meta.get('enabled_toolsets') on
the result of Session.load_metadata_only(). It returns a Session INSTANCE,
not a dict. .get() raised AttributeError, which the surrounding bare
except swallowed silently. The toolset chip in the UI saved correctly to
disk, but the streaming agent always ran with global toolsets.

Fix: use getattr(_session_meta, 'enabled_toolsets', None).

Two new regression tests:
- Source-level: forbid the .get() / [] dict-access shape.
- Runtime: Session.load_metadata_only must return a Session instance.

Full suite: 3604 passed, 0 failed.
2026-05-01 18:36:24 +00:00
nesquena-hermes f8007d43f3 v0.50.257: 4 Opus pre-release follow-ups + CHANGELOG + test fixes for #1415
stage-257 batch (PRs #1402 + #1415):

Opus pre-release advisor caught 4 issues in stage-257:

1. MUST-FIX (security): api/oauth.py::_write_auth_json — tmp.replace()
   preserves the temp file umask (0644 default), so OAuth access/refresh
   tokens landed world-readable on shared systems. Fix: tmp.chmod(0o600)
   BEFORE rename, with try/except OSError that warns but does not abort.

2. SHOULD-FIX: _handle_cron_history and _handle_cron_run_detail accepted
   job_id as a path component without validation. Mirrors the rollback
   path-traversal vector caught in v0.50.255 (#1405). Path() / .. does NOT
   normalize. New regex ^[A-Za-z0-9_-][A-Za-z0-9_.-]{0,63}$ with explicit
   . / .. rejection.

3. SHOULD-FIX: _handle_cron_history int(offset)/int(limit) raised
   ValueError on malformed input → confusing 500. Now try/except + clamp
   to (max(0, offset), max(1, min(500, limit))).

4. NIT: same regex applied to _handle_cron_run_detail (defense-in-depth
   even though path-resolve check would catch it downstream).

PR #1415 follow-up: 8 pre-existing tests in test_issue1106 and
test_custom_provider_display_name asserted bare model IDs but #1415
changes named-custom-provider IDs to @custom:NAME:model form when active
provider differs. Tests updated to use _strip_at_prefix helper to keep
checking the same invariant in the new shape.

4 regression tests in test_v050257_opus_followups.py + 8 fixed pre-existing
tests. Full suite: 3602 passed, 0 failed.
2026-05-01 18:30:41 +00:00
nesquena-hermes 42d4070e2d Merge PR #1415 from Thanatos-Z: fix named custom provider routing in model picker 2026-05-01 18:20:07 +00:00
nesquena-hermes bc17229a7d Merge PR #1402 from bergeouss: P2 improvements — cron history, toolsets per session, Codex OAuth
# Conflicts:
#	static/i18n.js
2026-05-01 18:20:05 +00:00
youzhi 59e07f3fff Fix WebUI custom provider routing 2026-05-02 02:11:41 +08:00
nesquena-hermes fcba6fda1c Merge PR #1411 from nesquena-hermes: TTS toggle CSS specificity collision (#1409) + Ollama env var bleed (#1410)
# Conflicts:
#	CHANGELOG.md
2026-05-01 17:34:28 +00:00
nesquena-hermes 5ce516ed38 v0.50.255: Opus follow-ups (4 fixes) + CHANGELOG
Opus pre-release advisor caught 4 issues in stage-255 (#1390 + #1405):

1. MUST-FIX: api/rollback.py path-traversal — _checkpoint_root() / ws_hash /
   checkpoint did NOT normalize Path() / "../escape", so an authenticated
   caller could read or restore from another allowlisted workspace via
   ../<other-ws-hash>/<sha>. New _validate_checkpoint_id() regex-guards
   with ^[A-Za-z0-9_-][A-Za-z0-9_.-]{0,63}$ and rejects . and .. literals.
   Both get_checkpoint_diff and restore_checkpoint validate.

2. SHOULD-FIX: redact_session_data perf cliff — the new api_redact_enabled
   toggle in #1405 called uncached load_settings() per string, recursed
   across messages[] and tool_calls[]. For a 50-message session: hundreds
   of disk reads per /api/session response. Now read once at the top and
   thread _enabled through via private kwarg.

3. SHOULD-FIX: voice-mode wrong-session TTS — the patched autoReadLastAssistant
   fires globally; if the user navigated to a different session between
   sending and stream completion, TTS would speak the wrong session\\s reply.
   New _voiceModeThinkingSid closure captures S.session.session_id at
   thinking-time; _speakResponse bails to _startListening() on mismatch.

4. NIT: rollback._inspect_checkpoint had bare Exception in the except tuple
   alongside specific catches, swallowing everything. Now (TimeoutExpired,
   OSError) only.

6 regression tests in test_v050255_opus_followups.py. Full suite: 3587 passed,
2 skipped, 3 xpassed.
2026-05-01 17:19:53 +00:00
nesquena-hermes 0e9bd651a4 fix: TTS toggle CSS specificity collision (#1409) + Ollama env var bleed (#1410)
Two unrelated UX/Settings bugs, both small surgical fixes with regression
tests.

Issue #1409 — TTS toggle has no effect
=======================================
Reported via Discord: ticking Settings → Voice → "Text-to-Speech for
responses" did nothing. The speaker icon never appeared on assistant
messages despite the checkbox saving to localStorage correctly.

Root cause (CSS specificity collision):
  static/panels.js _applyTtsEnabled() set
    btn.style.display = enabled ? '' : 'none'
  on every .msg-tts-btn. The '' branch removes the inline override, after
  which the .msg-tts-btn { display:none; } rule from style.css re-hides the
  button. Both branches left the icon hidden, so the toggle has been
  silently broken since #499 first shipped the TTS feature.

Fix (body-class toggle, Option B from the issue):
  - panels.js: _applyTtsEnabled now toggles body.classList('tts-enabled')
  - style.css: new compound selector
      body.tts-enabled .msg-tts-btn { display:inline-flex; align-items:center; }
  - default-hidden rule (.msg-tts-btn{display:none;}) preserved so the icon
    stays hidden by default (CSS-only state)
  - boot.js paths that already call _applyTtsEnabled(localStorage…) work
    unchanged — the new function applies state at the body level instead of
    inline-styling individual buttons, so the rule survives renderMd()
    re-renders without re-querying every button

Verified end-to-end against live server: getComputedStyle on a probe
.msg-tts-btn returns display:flex when body has tts-enabled, display:none
when it doesn't. Two regression tests in TestIssue1409TtsToggleBodyClass
explicitly check for the body-class shape and forbid the broken inline-style
pattern.

Issue #1410 — Ollama (local) shows "API key configured" when only
              Ollama Cloud key is set
=================================================================
Reported via Discord: configuring Ollama Cloud lit up the local Ollama card
too. Both providers were mapped to OLLAMA_API_KEY in api/providers.py
_PROVIDER_ENV_VAR.

Root cause:
  api/providers.py:47-48
    "ollama":       "OLLAMA_API_KEY",
    "ollama-cloud": "OLLAMA_API_KEY",
  _provider_has_key("ollama") found the value the user set for Ollama Cloud
  and returned True. But the runtime code path in
  hermes_cli/runtime_provider.py only consumes OLLAMA_API_KEY when the base
  URL hostname is ollama.com (Ollama Cloud) — local Ollama is keyless by
  default and reaches a custom base URL with no auth. The WebUI was
  reporting "configured" for a key local Ollama doesn't even read.

Fix (Option A from the issue body, preferred):
  - Drop bare "ollama" from _PROVIDER_ENV_VAR with an inline comment
    explaining why
  - _provider_has_key("ollama") falls through to the config.yaml branch,
    which already supports providers.ollama.api_key for local users who
    genuinely need to set a token
  - ollama-cloud retains its OLLAMA_API_KEY mapping unchanged

Verified end-to-end against live server with OLLAMA_API_KEY=sk-cloud-key-test
in env: GET /api/providers reports has_key=True only for ollama-cloud, and
has_key=False for bare ollama. Two regression tests in
TestIssue1410OllamaEnvVarBleed cover the bleed-prevention case AND the
"local user with config.yaml api_key still reports configured" case to
guard against over-correction.

Tests
-----
3572 passed, 2 skipped, 3 xpassed (was 3567 — added 5 new regression tests).

Closes #1409
Closes #1410

Reported by @AvidFuturist (Discord, May 1 2026)
2026-05-01 17:14:51 +00:00
nesquena-hermes 6ad7a4cc83 Merge PR #1405 from bergeouss: P3 features (insights, rollback, voice mode, subagent tree, redact toggle) 2026-05-01 16:58:49 +00:00
nesquena-hermes 6f55b973e5 Merge PR #1390 from starship-s: preserve session provider context 2026-05-01 16:58:48 +00:00
nesquena-hermes db548fc872 Merge PR #1392 from dso2ng: anchor active sessions per browser tab via /session/<id> URLs 2026-05-01 16:10:31 +00:00
bergeouss d9f3a69d29 fix: address PR #1405 review feedback — security, voice loop, locale coverage, test fixes
- Point 4 (security): _resolve_workspace now validates against known workspaces
  from workspaces.json to prevent arbitrary path write via restore endpoint
- Point 5 (voice mode): bail out of voice mode on not-allowed, service-not-allowed,
  and audio-capture errors instead of infinite retry loop
- Point 1 (locale coverage): added ~40 new English keys as placeholders with
  TODO:translate comments in zh, zh-Hant, ko, ru, es, de, pt locales
- Point 2 (test fix): tightened test regex to anchor on branch-indicator class
  to avoid collision with _sessionLineageKey helper
- Point 3 (test fix): accept both inline and parentEl variable forms for
  body.appendChild pattern in pinned indicator test

All 6 previously failing tests now pass.
2026-05-01 15:54:27 +00:00
bergeouss 3bff26037f fix: improve auth.json warning message and prevent credential ID collision
- Include file path and exception details in _read_auth_json warning log
- Add retry-on-collision (up to 3 attempts) for credential UUID generation

Addresses PR #1402 review feedback points 1 and 2.
2026-05-01 15:46:50 +00:00
bergeouss ae40af03d7 feat: P3 improvements — insights panel, rollback UI, voice mode, subagent tree, api redact toggle
- #464 Insights panel: usage analytics dashboard with session/message/token stats,
  model breakdown, activity by day/hour charts, token breakdown (GET /api/insights)
- #466 Rollback UI: checkpoint list, diff viewer, restore confirmation
  (api/rollback.py, GET /api/rollback/{list,diff}, POST /api/rollback/restore)
- #1333 Voice mode: turn-based STT→send→TTS loop using Web Speech API,
  progressive enhancement with pulsing indicator and auto-resume
- #494 Subagent session tree: parent→children grouping in sidebar with
  expand/collapse chevrons, child count badges, localStorage persistence
- #1396 API redact toggle: Settings checkbox to disable forced redaction for
  self-hosted users (lazy check at call-time, default ON)
- #1385 Closed: compact tool activity toggle already exists in Settings
- #497 Commented: proposed shared-file bridge for cross-process gateway approvals
- i18n: tab_insights added to all 8 locales, voice/checkpoint keys to EN+RU
2026-05-01 13:43:10 +00:00
bergeouss f4bfd9dca7 fix: address PR #1402 review feedback — cron sort, path traversal, OAuth robustness
- Cron history: sort by mtime instead of lexicographic filename (more robust)
- Path traversal: use resolve() + is_relative_to() instead of brittle string checks
- _cron_output_snippet: document the contract for response heading extraction
- _read_auth_json: catch JSONDecodeError specifically, log warning instead of silent swallow
- OAuth timestamps: use ISO strings consistently (created_at, updated_at)
- Credential id: use uuid4 instead of time-based truncated int (collision-safe)
2026-05-01 13:38:14 +00:00
bergeouss 8ae198e88c feat: P2 improvements — cron history, toolsets per session, Codex OAuth
- #468: Cron run history — GET /api/crons/history (metadata listing)
  + GET /api/crons/run (full output), lazy-load on click in Tasks panel
- #493: Per-session toolset override — Session.enabled_toolsets field,
  POST /api/session/toolsets endpoint, streaming handler override,
  composer chip UI with dropdown (matches reasoning chip pattern)
- #1362: In-app Codex OAuth — device-code flow (stdlib only, no httpx),
  SSE polling endpoint, onboarding wizard login button
- #1240: Design proposal comment for provider/model source-of-truth
2026-05-01 12:42:21 +00:00
bergeouss 51f3f30caf fix: P0 hotfixes — API regression, code block parser, chmod override
Fixes #1394 — _combined_redact() crashes with TypeError on older
hermes-agent builds that lack the 'force' kwarg in redact_sensitive_text().
Wrap the call in try/except to gracefully fall back.

Fixes #1397 — Two bugs in the code block tree-view renderer:
1. Newlines in data-raw HTML attribute are collapsed to spaces by the
   browser (HTML spec). Encode \n as &#10; to preserve multi-line content.
2. jsyaml lazy-load was never triggered when the library wasn't loaded yet.
   Now defers init and retries after _loadJsyamlThen() completes.

Fixes #1389 — fix_credential_permissions() now honors HERMES_SKIP_CHMOD=1
as a complete bypass, and when HERMES_HOME_MODE is set, only strips world
bits (0o007) instead of forcing chmod 0600 — preserving intentional group
access for Docker setups.
2026-05-01 12:10:48 +00:00
Dennis Soong 0ec4aad949 fix: anchor active sessions per browser tab 2026-05-01 19:52:05 +08:00
Hermes Agent 67193faf38 Apply Opus pre-release follow-ups for v0.50.253
Three small fixes from Opus review of the merged stage diff:

1. Strip 9 orphan wiki_* i18n keys (72 lines) from PR #1342 — leaked
   from a different branch, zero references outside i18n.js.

2. /branch endpoint: reject non-string session_id with explicit 400
   (was raising TypeError → generic 500 from get_session()).

3. /branch endpoint: reject negative keep_count with explicit 400
   (Python slice semantics on negative produces 'all but last N',
   confusing fork behavior).

Plus tests/test_v050253_opus_followups.py — 3 regression tests pinning
all three fixes.

Verified: 3558 pytest passing.
2026-05-01 06:53:32 +00:00
starship-s 1bfc4a992a Merge branch 'nesquena:master' into fix/provider-qualified-session-models 2026-05-01 00:35:43 -06:00
Hermes Agent 52bfceaa3b Add /branch command to fork conversations from any message (#1342, fixes #465)
Fix: gate parent_session_id emission in compact() on truthiness so
sessions without a fork link don't leak parent_session_id: None and
break the v0.50.251 lineage end_reason gating in agent_sessions.py.
The /branch endpoint sets the field on saved forks; everything else
keeps the v0.50.251 sidebar lineage path as the canonical source.
2026-05-01 05:32:45 +00:00
Hermes Agent fea47bd986 Heal 'provider: local' mid-conversation crash for local-model users (#1388, fixes #1384) 2026-05-01 05:29:42 +00:00
starship-s bdc328d034 fix: preserve webui model provider context
Persist session model_provider separately from model IDs so active/default provider selections like gpt-5.5 remain bare while routing through OpenAI Codex. Keep @provider:model for picker disambiguation and runtime bridging, and preserve explicit OpenRouter plus custom/proxy base_url routing.
2026-04-30 23:23:47 -06:00
Hermes Agent fc8898161e Apply Opus pre-release follow-ups (force redaction, log profile fallback) 2026-05-01 05:07:09 +00:00
Hermes Agent e36def33cd Show profile home in /status command (refs #463) (#1380) 2026-05-01 04:46:37 +00:00
Hermes Agent 838b931047 Keep API credential fallback redaction active (#1379) 2026-05-01 04:46:17 +00:00
Hermes Agent a6d831fc63 Cache /api/models/live with 60s TTL (#1378) 2026-05-01 04:46:15 +00:00
Hermes Agent d1e1c4eeec Fix CLI session import fallback model default (#1386) 2026-05-01 04:46:10 +00:00
nesquena-hermes f53556b3ff fix(cancel-stream): rename tool_calls to _partial_tool_calls (Opus MUST-FIX)
Opus pass-2 review of v0.50.251 caught a critical regression in PR
#1375:

The cancel-partial message stored captured tool calls under the
'tool_calls' key. That key is whitelisted by _API_SAFE_MSG_KEYS so
_sanitize_messages_for_api forwarded the entries to the next-turn
LLM call. But the captured entries use the WebUI internal shape
({name, args, done, duration, is_error}) — they don't have the
OpenAI/Anthropic id + function: {name, arguments} envelope. Strict
providers (OpenAI, Anthropic, Z.AI/GLM) would 400 on the malformed
entries. Net effect: the very cancel-then-continue scenario PR
#1375 aimed to improve becomes a hard fail.

Fix:
- Rename the persisted key to '_partial_tool_calls' (underscore-
  prefixed private key NOT in _API_SAFE_MSG_KEYS, so sanitize
  correctly strips it).
- Update static/messages.js hasMessageToolMetadata check to also
  recognize _partial_tool_calls for UI rendering.
- Update test_issue1361_cancel_data_loss.py assertion to check
  _partial_tool_calls (and tool_calls as legacy fallback).

Plus 2 NIT fixes from the same Opus review:

NIT 1 (api/profiles.py:153): re.match → re.fullmatch for consistency
with other _PROFILE_ID_RE callers in the codebase. The trailing-
newline footgun ($ matches before final \n in re.match) is now
closed. Without #1373's is_dir() guard, a name like 'valid\n' would
have created a directory named 'valid\n' on Linux. Doesn't escape
<HERMES_HOME>/profiles/ via Path joining, but unintended.

NIT 2 (test_issue798.py): R19j coverage gaps — added trailing-
newline tests, length-boundary tests (64-char valid, 65-char
rejected), single-char minimum, and non-ASCII / Unicode-trick tests.

New regression test (tests/test_pr1375_partial_tool_calls_sanitize.py):
- test_partial_tool_calls_field_not_forwarded_to_llm: pins that
  sanitize-for-API strips _partial_tool_calls + reasoning + does
  NOT have tool_calls on a partial message
- test_legitimate_tool_calls_are_preserved_for_completed_turns:
  pins that real OpenAI-shape tool_calls on completed turns survive
  sanitize unchanged

Tests: 3486 passing (3484 → 3486, +2 sanitize tests).
2026-04-30 23:43:23 +00:00
bergeouss f14280e2c4 fix(#1195): route sessions to profile dir even when dir doesn't exist yet (#1373)
When a user switched profiles and created a new session, the session
was saved to the default profile directory instead of the active
profile directory — because get_hermes_home_for_profile() silently
fell back to _DEFAULT_HERMES_HOME when the profile directory didn't
exist yet on disk.

Root cause: api/profiles.py:156 had `if profile_dir.is_dir(): return
profile_dir; return _DEFAULT_HERMES_HOME`. New profiles (no session
yet, so no dir) routed every session back to default.

Fix: remove the is_dir() guard, return the profile path
unconditionally. The profile directory is created on first use by
the agent/session layer.

5 regression tests in tests/test_issue1195_session_profile_routing.py:
existing-profile, non-existent-profile (the core fix), None, empty-
string, 'default' all return the expected path.

Co-authored-by: bergeouss <bergeouss@users.noreply.github.com>
2026-04-30 23:24:31 +00:00
bergeouss c5f4f569d6 fix(#1361): preserve reasoning, tool calls, and partial output on Stop/Cancel (#1375)
Three distinct data-loss paths fixed:

§A — Reasoning text was accumulated in a thread-local _reasoning_text
inside _run_agent_streaming. cancel_stream() never saw it because it
went out of scope when the thread was interrupted. Now mirrored to a
new shared dict STREAM_REASONING_TEXT keyed by stream_id, populated
in on_reasoning() and the reasoning branch of on_tool(), read in
cancel_stream().

§B — Live tool calls in thread-local _live_tool_calls were similarly
invisible to cancel_stream(). Now mirrored to STREAM_LIVE_TOOL_CALLS
on tool.started + tool.completed.

§C — Reasoning-only streams produced no partial message because the
thinking-block regex strip returned empty string and the `if _stripped:`
guard skipped the append. Now appends the partial message when EITHER
content text, reasoning trace, OR tool calls exist.

Mirrors the existing STREAM_PARTIAL_TEXT pattern from #893 exactly:
same dict creation in _run_agent_streaming, same _live_config fallback
in cancel_stream, same cleanup in _periodic_checkpoint.

8 regression tests in tests/test_issue1361_cancel_data_loss.py
covering all three sections plus tools+text combinations.

Co-authored-by: bergeouss <bergeouss@users.noreply.github.com>
2026-04-30 23:24:29 +00:00
nesquena-hermes 63251ad206 release: apply Opus SHOULD-FIX 1+2 + add #1372 manual-cron persistence
Opus pre-release findings on #1370 applied:

SHOULD-FIX 1: Tightened parent_session_id exposure to only emit when
the parent's end_reason is in {compression, cli_close}. Without this,
two distinct WebUI sessions sharing a non-continuation parent (e.g.
'user_stop') would get clustered by frontend's _sessionLineageKey
(which falls through to parent_session_id when _lineage_root_id is
missing) and incorrectly collapsed into a single sidebar row.

  Updated assertions in:
  - tests/test_session_lineage_metadata_api.py::
    test_non_compression_state_db_parent_does_not_create_sidebar_lineage
  - tests/test_pr1370_lineage_metadata_perf_and_orphan.py::
    test_non_compression_parent_does_not_extend_lineage

SHOULD-FIX 2: Chunked the IN-clause to 500 vars to stay under
SQLITE_MAX_VARIABLE_NUMBER. Python 3.9 ships sqlite 3.31 with the
default limit of 999. A power user with 2000+ sessions in the
sidebar would hit OperationalError, the silent except-wrapper would
swallow it, and lineage collapse would never work. Added
test_in_clause_chunked_for_large_session_set with SQL interception
to lock the invariant in source.

PR addition (per user directive — Opus + my review, no second
independent review round needed for combined batch):

#1372 from @NocGeek — fix: persist manual cron run results.
Self-contained 89 LOC fix split out from the held #1352. Mirrors the
scheduled-cron path (cron/scheduler.py:1334-1364) exactly: saves
output, marks job complete, treats empty response as soft failure
with matching error string. 2 behavioral tests using sys.modules
monkeypatch to mock cron.scheduler.run_job. CI not yet attached
because branch is brand-new; ran the new tests + adjacent suites
locally — all pass.

Final test count: 3471 passing, 0 failed.

Also adds 2 more regression tests for the perf-fix invariants:
- test_in_clause_chunked_for_large_session_set
- test_two_children_sharing_non_continuation_parent_not_collapsed
2026-04-30 23:17:54 +00:00
NocGeek 89dcab8327 fix: persist manual cron run results (#1372)
Manual WebUI cron runs previously called cron.scheduler.run_job(job)
and then only cleared the in-memory running flag. That meant output
could be dropped and job metadata like last_run_at / last_status was
not updated after a manual run.

This PR matches the scheduled cron path (cron/scheduler.py:1334-1364)
exactly:
- Save manual-run output via save_job_output
- Mark manual runs complete via mark_job_run
- Treat empty final_response as a soft failure with the same error
  string as the scheduled path
- Record manual-run failures in job metadata via mark_job_run(False)
- Keep _run_cron_tracked self-contained for worker-thread execution

Includes 2 behavioral regression tests using monkeypatch.setitem on
sys.modules to mock cron.scheduler.run_job + cron.jobs helpers — the
right test pattern (exercises the real _run_cron_tracked code path).

Split out from #1352 (the larger profile-aware-cron-panel PR that's
on hold) per pre-release-review feedback. Self-contained, doesn't
touch the held PR's profile-filtering scope.

Co-authored-by: NocGeek <NocGeek@users.noreply.github.com>
2026-04-30 23:15:31 +00:00
nesquena-hermes 571cfed180 release: v0.50.251 (#1370 perf fix + orphan-parent guard + regression suite)
Bundles:
- #1370 fix: expose session lineage metadata in API (@dso2ng)

Pre-release fixes applied:

1) Perf: replaced full table scan with parameterized WHERE id IN (...)
   query. Original code did SELECT id, parent_session_id, end_reason
   FROM sessions on every sidebar refresh. Measured 9ms cached scan at
   1000 rows in production (up to ~450ms cold-cache); scales linearly.
   New approach hits PRIMARY KEY + idx_sessions_parent — 50x faster
   at 1000 rows, ~0.2ms regardless of total row count. Depth-bounded
   to 20 hops to cap query count under pathological data.

2) Orphan-parent guard: suppress parent_session_id in API output when
   the referenced parent row doesn't exist in state.db. The frontend's
   #1358 _sessionLineageKey falls through to parent_session_id when
   _lineage_root_id is missing — orphan references would create
   never-collapsing single-row groups in the sidebar.

3) Regression suite (5 tests in
   test_pr1370_lineage_metadata_perf_and_orphan.py):
   - Pins the no-full-scan invariant by intercepting all SQL queries
     and asserting no SELECT FROM sessions without a WHERE clause
   - Pins orphan-parent suppression
   - Pins cycle termination via threading.Event watchdog (2s timeout)
   - End-to-end test for 4-segment compression chain root resolution
   - Pins non-compression end_reason boundary stops walk
2026-04-30 23:06:37 +00:00
Dennis Soong 7da1e074e4 fix: expose session lineage metadata in API (#1370)
PR #1358 added the client-side lineage collapse helper, but
/api/sessions often did not include _lineage_root_id for the WebUI
JSON sessions visible in the sidebar. In that case the helper has no
grouping key and multiple same-title continuation rows remain visible.

This PR:
- Reads parent_session_id and end_reason from state.db.sessions for
  the WebUI sidebar's session ids
- Walks the parent chain when end_reason is 'compression' or
  'cli_close', producing _lineage_root_id and _compression_segment_count
- Cycle-detects via a 'seen' set
- Preserves projected lineage metadata on imported/gateway session rows
- Allows sidebar collapse to group cross-surface continuation chains
  (CLI-close → WebUI continuation) while keeping non-continuation
  parent rows flat

Co-authored-by: Dennis Soong <dso2ng@gmail.com>
2026-04-30 23:04:49 +00:00
Nathan Esquenazi 604b44a254 fix(clarify-sse): inline snapshot under _lock to avoid deadlock in handler
The new _handle_clarify_sse_stream handler in #1355 holds clarify._lock and
then calls clarify.get_pending(sid) under the lock. get_pending also acquires
_lock internally — and clarify._lock is a non-reentrant threading.Lock(),
so the second acquisition deadlocks the SSE handler thread the moment any
client connects to /api/clarify/stream.

Existing tests pass because they only exercise sse_subscribe, sse_unsubscribe,
_clarify_sse_notify, and submit_pending directly — none of them invoke the
route handler. The deadlock would only manifest when a real EventSource opens
the connection.

Reproduced with a tiny harness that holds _lock and calls get_pending: the
worker thread is still blocked after a 2s timeout. With the fix, both empty
and populated queue cases complete in <1ms.

Fix: read clarify._gateway_queues / clarify._pending inline under the same
_lock acquisition, mirroring the approval SSE handler's pattern at
api/routes.py:2785-2793. No recursive lock; head-of-queue snapshot is
identical to what get_pending would have returned.

Added tests/test_pr1355_sse_handler_no_deadlock.py with three tests:
- behavioural: empty queue snapshot completes within 2s
- behavioural: populated queue snapshot returns the head entry
- source-level invariant: routes.py must not call get_clarify_pending()
  inside `with _clarify_lock:` block (locks the regression in)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 14:39:37 -07:00
fxd-jason d2d464aac3 feat(clarify): SSE long-connection for real-time clarify notifications (#1355)
Replaces the 1.5s HTTP polling loop for clarify with a Server-Sent Events endpoint at /api/clarify/stream that pushes clarify events to the browser instantly. Mirrors the approval SSE pattern from v0.50.248 (#1350) including all the correctness lessons:

- Atomic subscribe + initial snapshot under clarify._lock
- _clarify_sse_notify called inside _lock for ordering guarantees (no notify-out-of-order race)
- Notify passes head=q[0].data (head-fidelity, not the just-appended entry)
- resolve_clarify also calls notify after pop so trailing clarifies surface immediately (no stuck-clarify bug)
- Empty-state notify with None,0 after pop-empty so frontend hides the card
- 30s keepalive comments, _CLIENT_DISCONNECT_ERRORS handling
- Bounded queue (maxsize=16) with silent drop on full
- Frontend: EventSource with automatic 3s HTTP polling fallback on onerror

Co-authored-by: fxd-jason <wujiachen7@gmail.com>
2026-04-30 21:32:51 +00:00
Dennis Soong 70dac0135c fix: preserve imported session source metadata (#1357)
Session.load_metadata_only().compact() was dropping is_cli_session, source_tag, session_source, and source_label, so imported CLI/gateway sessions lost their provenance in sidebar/API payloads. Adds these to METADATA_FIELDS and Session.compact().

Co-authored-by: Dennis Soong <dso2ng@gmail.com>
2026-04-30 21:32:46 +00:00
nesquena-hermes bbdacdca5c fix: context window indicator overflow (#1356)
- api/streaming.py SSE payload now falls back to agent.model_metadata.get_model_context_length when compressor doesn't supply context_length (mirrors the session-save fallback shipped in v0.50.247).
- api/streaming.py also falls back to s.last_prompt_tokens to avoid using the cumulative input_tokens counter.
- static/ui.js tracks rawPct separately from pct and shows '(context exceeded)' tooltip when rawPct > 100 instead of misleading '100% used (0% left)'.
- static/messages.js clears 'Uploading...' composer status after upload completes.

Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-04-30 21:32:45 +00:00