Commit Graph

507 Commits

Author SHA1 Message Date
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 e3a2b0b3d2 v0.50.254: Opus follow-up + CHANGELOG
- popstate handler now refuses to switch sessions mid-stream (S.busy guard)
  Mirrors the same guard the cross-tab storage handler had. PR #1392 added
  the popstate listener but missed this. Without it, browser Back during
  a live stream silently yanks the user out of their turn.
  (Opus pre-release advisor finding)

- CHANGELOG entry for v0.50.254 (4 PRs + 1 Opus follow-up)

1 regression test in test_v050254_opus_followups.py.
2026-05-01 16:25:04 +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
nesquena-hermes 5d215c67c0 Merge PR #1398 from JKJameson: instant mouse click navigation, preserve tap-vs-drag cancel 2026-05-01 16:10:31 +00:00
nesquena-hermes ec4d543f8e Merge PR #1407 from franksong2702: rename CLI sessions → non-WebUI sessions in Settings 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
Frank Song 5679ef039c fix: rename 'CLI sessions' to 'non-WebUI sessions' in Settings toggle
The Settings toggle label previously said 'Show CLI sessions' or 'Show
agent sessions', but the feature actually surfaces conversations from
CLI, Telegram, Discord, Slack, WeChat, and other non-WebUI channels.

- Rename i18n key: settings_label_cli_sessions → settings_label_external_sessions
- Rename i18n key: settings_desc_cli_sessions → settings_desc_external_sessions
- Update all 8 languages (en, zh, zh-TW, ru, es, de, pt, ko)
- Reorder channel examples by global adoption: Telegram, Discord, Slack
- Update HTML fallback text to match new English strings
2026-05-01 22:40:53 +08: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 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
nesquena-hermes e258672bcb fix(sessions): instant mouse click navigation, preserve tap-vs-drag cancel on touch
Clicking a chat in the sidebar now processes immediately when using a mouse or
trackpad, but introduces a 300ms delay on touch devices to prevent accidental
navigation when a user scrolls the sidebar and lifts their finger mid-gesture.

Drag is detected when the pointer moves more than 5px from the pointerdown
position; a detected drag cancels any pending tap on release and suppresses
the hover highlight via a .dragging class added synchronously and removed
after a 50ms defer to prevent :hover activating before class removal settles.
The double-tap-to-rename path is unaffected.

Detection uses e.pointerType (already available on the pointerup event) rather
than user-agent sniffing.
2026-05-01 10:42:32 +01: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
starship-s 8439817c76 fix: keep profile placeholder refresh in switch path 2026-05-01 00:27:27 -06:00
Hermes Agent 8cd3680c0c Absorb starship-s commit cddd175: tighten composer spacing on 320px legacy phones
Pulls in the extra commit pushed to PR #1381 after our initial absorb. Adds a
@media (max-width: 340px) block that compacts gutters (composer-wrap padding,
composer-footer gap, composer-left gap) without shrinking the 44px touch
targets. Plus its regression test.

Verified with apply --check failed but actual apply succeeded — the failure
was due to context drift from our earlier CSS specificity fix; the new lines
landed at the correct location. test_mobile_layout.py: 47 tests passing.
2026-05-01 06:15:13 +00:00
Hermes Agent 1c356bf321 PR #1381 fix: prevent mobile-config-btn from leaking into desktop view (CSS specificity)
The .composer-mobile-config-btn{display:none} base rule was at line 896 but
.icon-btn{display:flex} (the button's other class) was at line 941 — equal
specificity, but later in source wins. Result: the button was visible at
desktop widths, sandwiched between the workspace and model chips.

Bumping the base rule's selector to .icon-btn.composer-mobile-config-btn
gives it specificity 0,0,2,0 (vs .icon-btn at 0,0,1,0), so it always wins
the cascade. The two narrow-viewport rules already use !important and remain
unaffected — desktop hides cleanly, mobile shows correctly.

Verified via Agent Browser CDP: 1440x900 desktop now shows the standard
chips only (no extra config button); iPhone 14 mobile shows the new compact
config btn at 44x44 with the panel toggling correctly. Screenshots:
/tmp/may2-shots/desktop-final.png, mobile-{closed,open}-final.png
2026-05-01 05:49:30 +00:00
starship-s 5c7c4c28e3 fix: add configured model group label 2026-04-30 23:45:46 -06:00
Hermes Agent 1a76e8761e Mobile composer layout: progressive-disclosure config panel + scoped titlebar safe-area (#1381) 2026-05-01 05:36:59 +00: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
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 e36def33cd Show profile home in /status command (refs #463) (#1380) 2026-05-01 04:46:37 +00:00
Hermes Agent 5c5ca7d2ef Intercept CLI-only slash commands in WebUI (#1382) 2026-05-01 04:46:30 +00:00
Hermes Agent d21c97205e Harden streaming scroll unpin behavior (#1360) (#1377) 2026-05-01 04:46:12 +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
nesquena-hermes f8754ded70 fix(autosave): guard preferences-autosave dirty-clear when password/model pending (Opus SHOULD-FIX Q1)
Pre-release Opus review of v0.50.250 caught a UX regression in PR
#1369: _autosavePreferencesSettings unconditionally cleared
_settingsDirty=false and hid the unsaved-changes bar on every
successful autosave. But password and model are still committed via
the explicit 'Save Settings' button (password for security; model
goes through /api/default-model). Race scenario:

  1. User opens System pane, types a new password (sets
     _settingsDirty=true; bar appears on close)
  2. User switches to Preferences, toggles any checkbox -> autosave
     fires -> _settingsDirty=false, bar permanently suppressed
  3. User closes panel -> _closeSettingsPanel short-circuits because
     !_settingsDirty -> typed password silently discarded
     (loadSettingsPanel blanks pwField.value='' on next open)

Same shape with model selector: pick a new default model, then
toggle any preference -> autosave fires -> no warning on close ->
model never persists.

Fix: only clear _settingsDirty and hide settingsUnsavedBar when both
the password field is empty AND the model selector matches its
on-open snapshot.

Pinned by an updated regression test asserting the conditional guard
exists.
2026-04-30 22:48:20 +00:00
Feco Linhares 645dfa25af fix: autosave preferences settings (#1369, fixes #1003 phase 2)
Phase 2 of #1003: extend the autosave pattern from the Appearance
panel to the Preferences panel so all preference changes are saved
automatically without requiring a manual 'Save Settings' click.

Mirrors the Phase 1 (Appearance) pattern exactly:
- 350ms debounce on field changes (500ms additional debounce on
  the bot_name text input — effective ~850ms latency for typing)
- Inline status feedback (saving / saved / failed + retry button)
- Clears dirty flag and hides unsaved-changes bar after successful save
- Password field excluded — still requires explicit save (security)
- Model selector excluded — still requires explicit save

13 fields now autosaving: send_key, language, show_token_usage,
simplified_tool_calling, show_cli_sessions, sync_to_insights,
check_for_updates, sound_enabled, notifications_enabled,
sidebar_density, auto_title_refresh_every, busy_input_mode, bot_name.

i18n keys (settings_autosave_saving/saved/failed/retry) already exist
in all 8 locales from Phase 1.

Co-authored-by: Feco Linhares <feco.linhares@gmail.com>
2026-04-30 22:38:44 +00:00
nesquena-hermes bc10a229e3 release: v0.50.250
Bundles 2 PRs:
- #1366 fix: guard finalizeThinkingCard with session ID check (with pre-release fix)
- #1367 fix(clarify-sse): stale-detector health timer (Opus SHOULD-FIX from v0.50.249)

Pre-release fix on #1366: the contributor's guard depends on
liveAssistantTurn.dataset.sessionId, but no code in the repo sets
that attribute. Without the fix, the guard would always early-return
(undefined !== sid is always true), breaking the streaming UI
completely — every assistant turn's thinking card would stay open
forever. Added per-site stamps at all 3 places that create
liveAssistantTurn in static/ui.js, plus a regression test that fails
any future creation site that forgets the stamp.
2026-04-30 22:27:40 +00:00
Josh d0257e8bcf fix: guard finalizeThinkingCard with session ID check (#1366)
Without this check, switching browser tabs while a stream is running
causes finalizeThinkingCard() to operate on the wrong session's
thinking card DOM — the card belongs to the stream that started it,
not the session currently displayed in the tab. The guard ensures
finalize only runs when the live assistant turn's session matches
the current session.

Co-authored-by: Josh <josh@fyul.link>
2026-04-30 22:25:25 +00:00
nesquena-hermes 2566b434d1 fix(clarify-sse): make health timer a stale-detector, not unconditional reconnect (#1367)
Follow-up to v0.50.249 / PR #1365 absorbing Opus SHOULD-FIX #2.
Originally reset out of #1365 because the reviewer flagged it as
out-of-scope; brought back per follow-up guidance that
correctness-improving changes should ship even when out of scope.

The clarify SSE health timer at static/messages.js:1715 was an
unconditional 60s force-reconnect, not the 'no event in 60s' detector
its comment claimed. Now actually a stale-detector that tracks
lastEventAt on initial+clarify event arrivals; only reconnects when
the gap exceeds 60s. Under healthy conditions the timer never fires.

Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-04-30 22:25:12 +00: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 6a736809ef fix: sync active session across tabs (#1359)
Adds a 'storage' event listener for the hermes-webui-session localStorage key. Idle tabs auto-load the new active session and re-render the sidebar; busy tabs show a toast and do not interrupt the active turn.

Co-authored-by: Dennis Soong <dso2ng@gmail.com>
2026-04-30 21:32:50 +00:00
Dennis Soong f13230f7cd fix: collapse sidebar session lineage rows (#1358)
When a session's compression lineage spans multiple segments (linked via _lineage_root_id from api/agent_sessions.py), the sidebar previously rendered each segment as a separate top-level row. Adds _collapseSessionLineageForSidebar() that groups by lineage root and keeps only the most recently active tip per group, with a _lineage_collapsed_count marker for future UI affordances.

Co-authored-by: Dennis Soong <dso2ng@gmail.com>
2026-04-30 21:32:48 +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
nesquena-hermes e68f74ac99 fix(approval): close SSE notify-ordering, head-fidelity, and trailing-approval gaps (Opus MUST-FIX A/C/D)
Pre-release Opus review caught three correctness bugs in the original
PR #1350 SSE wiring beyond the snapshot/subscribe race:

A) **Notify-ordering race (MUST-FIX A):** _approval_sse_notify took _lock
   only for the subscriber-list snapshot, then released it before
   put_nowait. With two parallel submit_pending calls, T2's notify
   could fire before T1's, leaving the UI showing pending_count=1 while
   the server actually had 2 queued.

C) **Trailing approval lost (MUST-FIX C):** _handle_approval_respond
   never called _approval_sse_notify after popping. With parallel
   tool-call approvals (#527), a second approval queued behind the one
   being responded to was invisible until the next event ever fired —
   in practice, the agent thread parked on it would appear hung.

D) **Payload showed tail not head (MUST-FIX D):** payload built from
   the just-appended entry instead of queue[0]. /api/approval/pending
   returns the head; SSE returned the tail. Diverging contracts.

Fix:
- Split into _approval_sse_notify_locked (caller holds _lock, no
  internal locking) and _approval_sse_notify (convenience wrapper).
- submit_pending: call _locked variant inside the queue-mutation lock,
  passing queue_list[0] as head.
- _handle_approval_respond: call _locked variant inside the pop lock,
  passing the new head (or None/0 if queue is empty).
- Restore fallback poll to 1500ms (was bumped to 3000ms; degraded-mode
  parity with v0.50.247 is more important than save 1.5s of polling).

New regression tests in tests/test_pr1350_sse_notify_correctness.py:
- test_second_submit_pending_sends_head_not_tail (D)
- test_respond_to_first_pushes_second_as_new_head (C)
- test_respond_to_only_pending_pushes_empty_state (C edge)
- test_pending_count_is_monotonic_under_contention (A)

Updated test_approval_sse.py to pin the new contract:
- _approval_sse_notify_locked(session_key, head, total)
- 1500ms fallback interval

Total: 3411 tests passing.

Co-authored-by: jasonjcwu <jasonjcwu@users.noreply.github.com>
2026-04-30 18:45:15 +00:00
fxd-jason 932694aec6 feat(approval): SSE long-connection for real-time approval notifications (#1350)
Replaces the 1.5s HTTP polling loop with a Server-Sent Events endpoint
at /api/approval/stream that pushes approval events to the browser
instantly. The backend uses a thread-safe subscriber registry
(_approval_sse_subscribers) with bounded queues to prevent memory
leaks from slow clients. Frontend uses EventSource with automatic
fallback to 3s HTTP polling on SSE error.

- Backend: subscribe/unsubscribe/notify lifecycle in api/routes.py
- New route: GET /api/approval/stream?session_id=
- submit_pending() now calls _approval_sse_notify() after queue append
- Frontend: EventSource with onerror -> _startApprovalFallbackPoll()
- 30s keepalive comments, _CLIENT_DISCONNECT_ERRORS handling
- 42 new tests (static analysis + unit + concurrency)

Co-authored-by: jasonjcwu <jasonjcwu@users.noreply.github.com>
2026-04-30 18:31:42 +00:00
fxd-jason 1df89e7a52 fix(ui): show context indicator percentage without explicit context_length (#1349)
Frontend companion to backend fix in v0.50.246 (#1341 + a5c10d5).
Default context window to 128K when usage.context_length is falsy.
Show '(est. 128K)' label when using the default.
Use input_tokens as fallback for last_prompt_tokens.

Co-authored-by: jasonjcwu <jasonjcwu@users.noreply.github.com>
2026-04-30 18:31:30 +00:00
nesquena-hermes eb678d5b54 feat(cron): auto-assign cron job sessions to dedicated 'Cron Jobs' project (#1079)
From PR #1345.

Co-authored-by: bergeouss <bergeouss@users.noreply.github.com>
2026-04-30 17:13:59 +00:00
nesquena-hermes d4b055c30b fix(streaming+ui): preserve user message on cancel + persist activity-panel expand state (#1298)
From PR #1338. Already independently APPROVED by nesquena before being absorbed into v0.50.246.

CHANGELOG entries from this PR were dropped during squash (the v0.50.245 section is already
shipped); they will be re-added under [v0.50.246] in the release commit.

Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-04-30 16:18:41 +00:00
nesquena-hermes 1fa740d32f feat(chat): render fenced code blocks in user messages (#1325)
From PR #1335.

Co-authored-by: bergeouss <bergeouss@users.noreply.github.com>
2026-04-30 16:18:02 +00:00
nesquena-hermes fbe84d26e6 fix(ui+pwa): avoid stale Mermaid render errors and bust cached static asset URLs on every release
From PR #1337.

Co-authored-by: Dennis Soong <dso2ng@gmail.com>
2026-04-30 16:18:01 +00:00
nesquena-hermes aa2b9d504d fix(mobile): workspace panel sliver + composer footer collapse (#1300)
From PR #1328.

Co-authored-by: Frank Song <franksong2702@gmail.com>
2026-04-30 15:24:36 +00:00
nesquena-hermes 4683a4a0d0 fix(models): default model rehydration when providers share slash-qualified IDs (#1313)
From PR #1326.

Co-authored-by: hacker2005 <chen20057275@outlook.com>
2026-04-30 15:24:35 +00:00
nesquena-hermes e86de0aff3 fix(ui): show configured fallback models missing from catalog
From PR #1322.

Co-authored-by: renatomott <renato.mott@gmail.com>
2026-04-30 15:24:34 +00:00
nesquena-hermes 1ccd958e23 fix(ui): avoid duplicate header copy buttons (#1096)
From PR #1324.

Co-authored-by: Dennis Soong <dso2ng@gmail.com>
2026-04-30 15:24:32 +00:00
nesquena-hermes eb95c6a341 fix(i18n): restore zh-Hant locale labels
From PR #1323.

Co-authored-by: Dennis Soong <dso2ng@gmail.com>
2026-04-30 15:24:31 +00:00
nesquena-hermes 3f838fc31a release: v0.50.244 (#1308)
release: v0.50.244

Batch release of 4 PRs:

- #1303 (@fecolinhares) — TTS playback of agent responses via Web Speech API.
  Per-message speaker button + auto-read toggle + voice/rate/pitch in
  Settings. localStorage-only state. Closes #499.

- #1304 — Stale saved session 404 cleanup + structured api() errors.
  Salvaged from #1084. Independently approved on 358275e.

- #1306 — Cmd/Ctrl+K works while a conversation is busy.
  Salvaged from #1084. Independently approved on 2e8a239.

- #1307 — Sienna skin (warm clay & sand earth palette).
  Salvaged from #1084. Independently approved on 5cd79c8.

Tests: 3290 passed, 2 skipped, 3 xpassed, 0 failures (was 3254; +36 tests).

Independently reviewed and approved by nesquena (commit 47f0e0d). End-to-end
trace verified the TTS flow; security audit confirmed SpeechSynthesisUtterance
is plain-text-only with no XSS surface; behavioural harness confirmed
_stripForTTS handles all 12 markdown-stripping cases; bounds clamping on
rate/pitch verified; opt-in behavior verified.
2026-04-29 21:34:27 -07:00