Commit Graph
1861 Commits
Author SHA1 Message Date
nesquena-hermes f4e150e105 Merge remote-tracking branch 'origin/master' into fix/4768-cron-graceful-degrade 2026-06-24 09:23:37 +00:00
nesquena-hermes 1e3132376b fix(#4768): only treat absent cron package as unavailable, re-raise real import bugs (Codex gate)
ModuleNotFoundError fires both when cron.jobs is absent AND when an existing
cron/jobs.py fails to import its own dependency. Catch as exc and only return the
graceful {jobs:[],cron_unavailable:true} when exc.name is 'cron'/'cron.jobs';
re-raise otherwise so a real cron bug isn't silently swallowed.
2026-06-24 09:12:46 +00:00
nesquena-hermes 34694a1f02 fix(#4768): degrade GET /api/crons gracefully when the cron module is absent
In split-container / minimal Docker deployments the WebUI image may not ship the
agent's cron package on its import path, so the Tasks tab's GET /api/crons did
'from cron.jobs import list_jobs' and 500'd with ModuleNotFoundError — breaking the
whole tab. Now the import is guarded and returns {jobs: [], cron_unavailable: true}
instead, so the Tasks tab loads (empty) instead of erroring. Happy path unchanged.
Co-authored-by: reporter
2026-06-24 08:42:50 +00:00
nesquena-hermes 1fc24ddf68 feat(#4241): add Vietnamese (vi) as 14th UI locale + wire parity
Maintainer-completed @thanhtantran's #4241 (Nathan approved bringing it in):
- vi locale block in static/i18n.js (contributor's 1615-line translation) rebased onto master.
- Added vi to _LOGIN_LOCALE in api/routes.py (login-page localization, issue #1442 parity).
- Bumped the per-locale hardcoded-count assertions 13->14 across the locale-parity
  tests (rtl, quota_chip, hide_suggestions, virtualize_transcript, no_git, help_tab,
  verdigris/zeus skins, auxiliary_models) and added 'vi' to the composer-voice-buttons
  LOCALES tuples. vi already carries all current English keys (verified 14x per key).
All 497 locale/parity tests pass. Co-authored-by: thanhtantran
2026-06-24 08:39:14 +00:00
nesquena-hermes 1fc86a47c7 Merge remote-tracking branch 'origin/master' into fix/4808-streaming-sessions-cache-ttl 2026-06-24 08:18:44 +00:00
nesquena-hermes 8eed95a25e fix(#4808): widen /api/sessions cache TTL while streaming to stop per-poll rebuilds
The frontend polls /api/sessions every 5s while a turn streams
(static/sessions.js _streamingPollMs=5000), but the cache TTL was 2.5s
(_SESSIONS_CACHE_TTL_SECONDS) — so every streaming poll landed in a fresh window
and forced a full all_sessions() rebuild on the hot path under the global store
LOCK, pinning CPU to 100% and starving token rendering on large stores
(recurrence of #4672). The #4680 freeze-marker only de-dupes rebuilds WITHIN one
TTL window; with polls spaced wider than the TTL it never helped.

Fix: while _session_list_cache_streaming_freeze_marker() indicates an active
stream, use _SESSIONS_CACHE_STREAMING_TTL_SECONDS=10.0 (> the 5s poll) so the
sidebar holds steady across streaming polls. Live runtime state (active stream,
sort order, pending flags) is overlaid on every response regardless of cache, and
structural/settings changes still evict immediately via the source stamp — only a
streaming session's own persisted title/message_count can lag up to 10s, which the
#4672 design already deems acceptable. Idle path byte-for-byte unchanged.

Diagnosis credit: investigation reproduced 6 streaming polls -> 6 rebuilds before,
1 after. Co-authored-by: batkuip <reporter>
2026-06-24 08:06:45 +00:00
nesquena-hermes 22b98e3922 Merge branch 'peek-4828' into gate/4828-reasoning-echo
# Conflicts:
#	CHANGELOG.md
2026-06-24 07:57:31 +00:00
Frank Song 2aaad7a282 Fix final answer reasoning echo 2026-06-24 15:29:25 +08:00
nesquena-hermes f3086f4974 Merge branch 'peek-4809' into gate/4809-worklog-dur
# Conflicts:
#	CHANGELOG.md
2026-06-24 07:25:32 +00:00
nesquena-hermes b9f1491de7 Merge branch 'peek-4798-v2' into gate/4798-v2 2026-06-24 06:25:59 +00:00
Frank Song cafeda63ca Preserve processed Worklog duration during message merges 2026-06-24 11:32:27 +08:00
nesquena-hermes 5253724f90 rebase(#2814): resolve i18n onto current master + add kanban_view_* keys to all 13 locales
Took master's i18n.js as base (it has all 13 locale blocks + had reorganized the
kanban keys, so the stale-branch conflict block would have duplicated them in
zh-Hant), then inserted #2814's 5 kanban_view_* keys into every locale block after
kanban_new_task — real translations for it/ja/zh/zh-Hant/fr (from the PR), English
fallback elsewhere. All 13 locales now have exactly 1 of each key; locale-parity +
kanban tests green (291 passed).
2026-06-23 23:40:45 +00:00
Rod Boev 53f18556fb Keep model refresh builds authoritative until cache save completes 2026-06-23 17:53:36 -04:00
Rod Boev fdedb2078a Keep session-visit model refresh from restamping stale catalogs 2026-06-23 17:40:09 -04:00
nesquena-hermes 48663a4af3 Merge branch 'peek-4788' into gate/4788-json-view 2026-06-23 21:19:18 +00:00
nesquena-hermes 7db3aa123f Merge branch 'peek-4656' into stage/4656-escape-readonly 2026-06-23 20:43:53 +00:00
Charles Bine dd66640fae feat(#484): configurable default view for JSON/YAML code blocks
The JSON/YAML tree viewer (#484) hardcoded its default: valid json/yaml
fenced blocks opened in Tree view at 10+ lines (`const showTree=lineCount>=10;`).
Make that default user-configurable while preserving current behavior.

- New settings (persisted via /api/settings -> settings.json):
  - structured_code_default_view: auto | on | off  (default auto)
  - structured_code_auto_tree_lines: int 1..1000    (default 10)
- ui.js: replace the hardcode with a sanitized, unit-testable decision
  helper _structuredCodeShowTree(mode, threshold, lineCount); Tree/Raw
  toggle, Raw <pre><code>, parse-failure fallback, and YAML lazy-load
  are unchanged.
- Appearance settings: 'JSON/YAML code blocks' select + 'Auto threshold
  lines' input (disabled outside auto mode); wired through the existing
  appearance-autosave and explicit-save paths; live re-render on change.
- boot.js seeds window globals; i18n labels added to all 13 locales.
- Default (auto + 10) reproduces the original >=10 behavior exactly.
2026-06-23 13:51:45 -04:00
nesquena-hermes 235bfffaf0 Merge remote-tracking branch 'origin/master' into peek-4741
# Conflicts:
#	CHANGELOG.md
2026-06-23 17:06:17 +00:00
Rod Boev e5aa548d67 fix(#4756): refresh stale model catalogs on session visit 2026-06-23 12:50:28 -04:00
Rod Boev 165433fea6 Keep legacy sidebar tests aligned with visible-row filtering 2026-06-23 11:56:25 -04:00
Rod Boev e9baeff2b2 fix(sidebar): preserve attention rows in filtered session payloads (#4775) 2026-06-23 10:51:33 -04:00
Rod Boev e27b7cf042 perf(sidebar): trim hidden and empty sidebar payload rows (#4775) 2026-06-23 10:29:06 -04:00
boriken72 c6994b501a perf: cache app-shell template instead of re-reading index.html per request
The `/`, `/index.html`, and `/session/<id>` routes are the hottest
navigations. Each request re-read the ~190 KB static/index.html from disk
and re-applied two process-constant token substitutions
(__WEBUI_VERSION__, __MAX_UPLOAD_BYTES__), then ran inject_extension_tags()
(which itself reads the extension state + manifest from disk).

WEBUI_VERSION and MAX_UPLOAD_BYTES are fixed for the process lifetime, so
this caches the partially rendered template in an in-process cache keyed by
(size, mtime_ns) -- mirroring the existing _STATIC_CACHE pattern in this
file -- so a redeploy is still picked up without a restart. The per-session
CSRF token and runtime extension tags are still applied per request, so
output is byte-identical to before.

Verified: byte-identical to the old inline render, CSRF still varies per
request, cache invalidates on mtime change, ~5x faster shell render in a
local microbenchmark. Adds tests/test_index_shell_template_cache.py.
2026-06-23 09:01:59 -04:00
Rod Boev 73a209aca7 fix(#4766): close review gaps in sidebar source filtering 2026-06-23 06:10:16 -04:00
Rod Boev 9a5a3eb6d5 fix(#4766): filter /api/sessions by active source bucket 2026-06-23 05:56:00 -04:00
Rod Boev 40fc69b1cf Require real browser origins for escape grant minting (#4582) 2026-06-23 02:53:12 -04:00
Rod Boev decb1037bc Keep escape grants browser-confirmed and refreshable (#4582) 2026-06-23 02:53:12 -04:00
Rod Boev e8c4cb85c3 Preserve explicit inline markers for HTML preview guards 2026-06-23 02:53:12 -04:00
Rod Boev d20e9dd852 Prevent stale escape errors from collapsing live browse grants 2026-06-23 02:53:12 -04:00
Rod Boev 29f38902be Keep escape navigation on the tested UI route path (#4582)
# Conflicts:
#	static/workspace.js
2026-06-23 02:53:12 -04:00
Rod Boev 3401f589a7 Prevent file escape grants from breaking anchored reads (#4582) 2026-06-23 02:52:44 -04:00
Rod Boev 8caca99cae Preserve escape grants while locking second-hop regressions 2026-06-23 02:52:44 -04:00
Rod Boev 0f80113c12 feat(workspace): allow browser-only escape-target navigation (#4582)
# Conflicts:
#	static/workspace.js
2026-06-23 02:52:44 -04:00
nesquena-hermes 2f250a4198 review(#4741): address greptile P2s — drop redundant lazy import, deep-copy raw config cache
Two greptile findings on #4741:
- api/routes.py: removed the redundant 'from api.config import load_settings'
  inside _session_list_payload_to_response — load_settings is already imported at
  module scope, and this function only runs at request time (after module load).
- api/config.py: _load_yaml_config_file_raw now deep-copies the cached raw dict by
  default so a caller can never mutate the shared _yaml_file_cache entry. The
  internal _load_yaml_config_file passes _copy=False (it immediately runs
  _expand_env_vars, which returns a fresh structure and never mutates its input)
  so the /api/reasoning hot path keeps its cheap no-copy fast path; reload_config
  uses the safe default copy.
2026-06-23 05:43:17 +00:00
nesquena-hermes 001ee0a986 fix(#4662): stamp _cfg_mtime for empty-dict config (Opus gate finding)
The 'and loaded' guard skipped the _cfg_mtime stamp for a config that parses to
an empty {} dict, so get_config()'s current_mtime != _cfg_mtime stale check fired
on every call and re-entered reload_config() under _cfg_lock forever — a hot-path
spin for a freshly created/reset profile's {} config. Split the guard: the no-op
_cfg_cache.update is still skipped for {}, but _cfg_mtime is always stamped when
the file parsed to a dict, matching master's pre-#4662 behavior. Incidentally
fixes the pre-existing empty/None-config spin. Adds a regression test.
2026-06-23 05:43:17 +00:00
nesquena-hermes 8006db22fc perf(#4662): route reload_config parse through the mtime config cache
Phase 2 of #4662. reload_config() did a raw yaml.safe_load of config.yaml on
every call (~125ms+ on a large file), including on the profile-switch hot path
(~446ms load_settings stage in /api/sessions diagnostics). It now parses through
the mtime+size-keyed cache added in #4652, via a shared _load_yaml_config_file_raw
core (DRY: _load_yaml_config_file = raw + env-expand). reload_config still runs
its OWN env expansion pinned to the unscoped process-env view (block_process_env_fallback
reset + _thread_ctx.env={}) so the #798 TLS profile-isolation invariant is byte-for-byte
preserved; only the disk parse is now cached. An unchanged config.yaml is no longer
reparsed; an mtime change still busts the cache.
2026-06-23 05:42:32 +00:00
nesquena-hermes 4f071895cc perf(#4662): read redaction setting once per sidebar response, not per row
Phase 3 of #4662. _session_list_payload_to_response read settings.json once
per row via _redact_text's per-call load_settings() fallback (the multi-second
response_write stage in /api/sessions diagnostics on large lists). The
_sidebar_session_response_item -> _redact_text(_enabled=...) threading already
existed; this wires the caller to read the api_redact_enabled flag once and
thread it through every row, matching the read-once optimization
redact_session_data() already uses. Behavior-preserving; redaction still
applies (or not) exactly as before.
2026-06-23 05:42:32 +00:00
Frank Song a4470bd30c Deduplicate live progress reasoning echoes 2026-06-23 12:51:16 +08:00
nesquena-hermesandwlknight 8f5ffea4a1 fix(#4729): guaranteed-exit reasoning flush in inner finally (Codex re-gate 2)
Codex re-gate 2: post-run flush is bypassed on exception (jumps to outer except) + retry
paths, so a buffered reasoning tail was lost before apperror there. Added a guarded
_flush_reasoning_buffer() at the top of the inner finally (guaranteed exit for
normal/exception/retry) so the tail always reaches the live Thinking view before teardown.
+ regression asserting the finally flush.

Co-authored-by: wlknight <wlknight@users.noreply.github.com>
2026-06-23 02:44:34 +00:00
nesquena-hermesandwlknight fa51e3413d fix(#4729): flush reasoning buffer at real boundaries, not just on_reasoning(None)
Codex re-gate caught a SILENT gap: the tail-flush only ran on on_reasoning(text=None),
but the agent never calls reasoning_callback(None) — so the last sub-100ms coalesced
buffer was silently lost, and a reasoning→tool/output transition stranded the tail.

Added a shared _flush_reasoning_buffer() helper called at every boundary that closes or
reorders the live reasoning stream: on_token (visible output starting), on_tool (tool
boundary), the defensive on_reasoning(None), and right after agent.run_conversation()
returns (terminal catch-all for a turn that ends on reasoning with no trailing token/tool).
Each flush emits the buffered chunk then clears it (since-last-flush delta semantics, so
the frontend's reasoningText += text stays correct). + regression test for all sites.

Co-authored-by: wlknight <wlknight@users.noreply.github.com>
2026-06-23 02:28:52 +00:00
wlknight b85e2c84ae fix: throttle reasoning SSE events to ~10 Hz using coalescing buffer 2026-06-23 09:48:02 +08:00
nesquena-hermesandrodboev 4ae41c6d7a fix(#3961): add Anthropic OAuth tokens to fail-closed floor (agent-absent CI/test env)
The registry-derived scrub union is best-effort and returns empty when
hermes_cli.auth can't be imported (WebUI-only CI/test env). That made
ANTHROPIC_TOKEN / CLAUDE_CODE_OAUTH_TOKEN scrubbing depend on the agent being
installed, so the quota-subprocess regression failed on CI (agent absent) while
passing locally (agent present). Add both token names to the fail-closed floor
set so the scrub covers them regardless of the registry import — strictly more
correct (defense-in-depth) and deterministic across environments.

Co-authored-by: rodboev <rodboev@users.noreply.github.com>
2026-06-22 21:07:54 +00:00
nesquena-hermesandrodboev d2ee837796 fix(#3961): resolve Opus ship-gate findings (startup NameError + reload cache poisoning)
Opus final gate reproduced two issues in the config-expansion hardening:

MUST-FIX — import-order forward reference. _expand_env_vars (config.py top) calls
_thread_local_env_value, but that fn + _thread_ctx were defined ~6900 lines later,
AFTER the import-time reload_config(). A ${VAR} in a startup config.yaml raised a
(swallowed) NameError, leaving import-time config consumers with empty config.
Fix: relocate _thread_ctx + _thread_local_env_value above the config-file section
(they depend only on os + threading). Verified: startup ${VAR} now expands, no
NameError.

SHOULD-FIX — reload_config baked block-scoped expansion into the process-global
_cfg_cache. If a reload fired while a readonly/worker scope was active (profile
alternation resolves _get_config_path to the named profile), under-expanded
literal ${VAR}s were written to the shared cache, starving concurrent readers of
the `cfg` alias. Fix: reload_config now expands the global cache with the
block flag forced off (process-env view), since the cache is process-global;
per-request scoped reads still go through _thread_local_env_value at their own
sites. Verified: global cache holds the process-expanded value, not the literal.

Co-authored-by: rodboev <rodboev@users.noreply.github.com>
2026-06-22 20:18:01 +00:00
nesquena-hermesandrodboev d60ad140b1 fix(#3961): scrub Azure managed-identity (MSI/IMDS) credential-source env vars
Final credential-surface item: agent/azure_identity_adapter.py treats
IDENTITY_ENDPOINT / MSI_ENDPOINT as ManagedIdentityCredential sources, so an
empty named profile could inherit the host's managed identity on the quota +
detached-worker paths. Add IDENTITY_ENDPOINT / IDENTITY_HEADER / MSI_ENDPOINT /
MSI_SECRET to the non-registry scrub set (config var AZURE_FOUNDRY_BASE_URL kept).
Detached-worker regression extended.

This completes the agent's enumerable credential-env read surface (verified from
source). Decision (with maintainer): stop the Codex deep-gate here — the
remaining theoretical surface is cloud-SDK-internal credential-source endpoints
on pre-existing infra, not vectors introduced by this PR; validated via Opus +
full suite. The PR's #3961 scope (os.environ mirror race on /api/providers +
/api/models) is fully closed plus the broader same-class hardening.

Co-authored-by: rodboev <rodboev@users.noreply.github.com>
2026-06-22 19:58:44 +00:00
nesquena-hermesandrodboev 51dd69b4f2 fix(#3961): route config ${VAR} expansion through the profile-aware env lookup
Codex re-gate found a config-template leak vector distinct from the env-name
scrub: _expand_env_vars() expanded ${VAR} references in config values via raw
os.environ, so a profile config.yaml of e.g. `api_key: ${ANTHROPIC_TOKEN}` let
_get_provider_api_key reconstruct the server-process token and pass it through as
the API key argument — even when the child-env scrub stripped the env var. This
bypassed the thread-local channel entirely.

_expand_env_vars now substitutes via _thread_local_env_value (the same
thread-local-first accessor with the block_process_env_fallback guard the PR
introduced): a ${VAR} resolves to the profile's own value, and refuses the
process-env fallback when a profile-scoped readonly/background scope set the block
flag (leaving the reference unexpanded). Cache integrity preserved — only the RAW
unexpanded dict is cached; expansion re-runs per read against the current thread's
scope. Normal (no-scope) behavior unchanged. + regression that fails without the fix.

Co-authored-by: rodboev <rodboev@users.noreply.github.com>
2026-06-22 19:49:44 +00:00
nesquena-hermesandrodboev bf548a255d fix(#3961): complete cloud-credential scrub set (AWS container FULL_URI + Azure identity family)
Round-3 gate found two more credential env vars the agent resolves cross-profile
that the scrub set missed, verified against the installed agent source:
- AWS_CONTAINER_CREDENTIALS_FULL_URI — read by model_switch._has_aws_creds:1360
  (botocore container-credentials provider; RELATIVE_URI was present, FULL_URI
  was not).
- Azure identity family — AZURE_CLIENT_ID / AZURE_CLIENT_SECRET / AZURE_TENANT_ID
  / AZURE_FEDERATED_TOKEN_FILE (agent/azure_identity_adapter.py service-principal
  + workload-identity model auth) and AZURE_FOUNDRY_API_KEY
  (runtime_provider.py).

Enumerated the agent's full credential-env read surface from source to close the
whole class in one pass rather than one var per gate round. Deliberately EXCLUDES
config/region vars (AWS_REGION, AWS_DEFAULT_REGION, AZURE_FOUNDRY_BASE_URL) — the
child probe may legitimately need those. Regressions extended to pin FULL_URI +
the Azure family.

Co-authored-by: rodboev <rodboev@users.noreply.github.com>
2026-06-22 19:27:00 +00:00
nesquena-hermesandrodboev 4eb069be97 fix(#3961): complete credential scrub with non-registry agent creds (CUSTOM_API_KEY, AWS/Bedrock)
Codex re-gate found the registry-derived scrub still missed credential env vars
the agent resolves via raw os.getenv() that aren't in PROVIDER_REGISTRY:
CUSTOM_API_KEY (hermes_cli/models.py), AWS_BEARER_TOKEN_BEDROCK
(hermes_cli/model_switch.py), AZURE_ANTHROPIC_KEY (runtime_provider.py), and the
boto3 AWS_* credential family (agent/bedrock_adapter.py). Same cross-profile leak
class on custom/AWS-backed provider quota + detached-worker paths.

- Add _NON_REGISTRY_AGENT_CREDENTIAL_ENV_NAMES fail-closed set, union into
  _profile_secret_env_names so BOTH leak sites pick it up.
- _account_usage_subprocess_env now relies on the centralized scrub set (WebUI-
  only fallback only if the api.profiles import fails), dropping the partial
  local AWS pair.
- 2 more regressions (quota subprocess + detached worker) covering CUSTOM_API_KEY
  + AWS family, fail without the fix.

Co-authored-by: rodboev <rodboev@users.noreply.github.com>
2026-06-22 19:19:56 +00:00
nesquena-hermesandrodboev 1293f0301e fix(#3961): scrub agent-registry OAuth token env vars in profile credential scope
Codex re-gate of #4544 reproduced a residual cross-profile leak: the PR closed
OPENROUTER_API_KEY but ANTHROPIC_TOKEN / CLAUDE_CODE_OAUTH_TOKEN still leaked the
server-process credential to an empty named profile on /api/provider/quota
(anthropic) and the detached-worker model-rebuild path, because the scrub set was
built only from the WebUI _PROVIDER_ENV_VAR map (which omits OAuth/token-flow
provider env vars).

Widen the shared _profile_secret_env_names() scrub source to union in the agent
registry's api_key_env_vars (hermes_cli.auth.PROVIDER_REGISTRY) so every
credential env var the agent runtime can resolve via raw os.getenv() is scrubbed
in profile-scoped reads. Fixes both leak sites at once (both route through
_profile_secret_env_names). + 2 regressions that fail without the fix.

Co-authored-by: rodboev <rodboev@users.noreply.github.com>
2026-06-22 19:09:59 +00:00
nesquena-hermes 239529f9bd stage #4544 (rodboev): route provider/model credential reads through thread-local profile channel (fixes #3961) 2026-06-22 18:47:59 +00:00
Frank Song 0567b056c0 fix(compression): guard state replay after compacted anchors 2026-06-22 18:37:05 +00:00