2a804ae38d0f67cb5bba4b83d533aa2ce8b40cad
2721
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
2a804ae38d | Merge PR #7402 for gate | ||
|
|
edf6c6a686 | Merge PR #7288 for re-gate on current master | ||
|
|
e69d681e82 | Merge PR #6620 for exact-head re-gate | ||
|
|
e7841a5988 |
gate fixes: opt-in pipe separator (#7331) + IPv6-safe Origin (#6772)
Two Codex adversarial-security-gate findings on the batch: 1. #7331 CORE (backward-compat break): making '|' an unconditional trusted-groups separator would re-split an existing deployment whose group NAME legitimately contains a literal '|' (e.g. 'unprivileged|admins' read as one group), silently changing its profile binding. Pipe splitting is now OPT-IN via HERMES_WEBUI_TRUSTED_GROUPS_PIPE_SEPARATOR; comma/newline stay the default. Tests: literal-pipe preserved by default, pipe splits only with the flag set. 2. #6772 follow-up (my own hardening introduced it): rebuilding the origin from parsed parts dropped the brackets around an IPv6 host, so 'http://[::1]:8787' became 'http://::1:8787' and never matched the browser's bracketed clientDataJSON.origin -> passkey login over IPv6 would fail. Re-bracket IPv6 literals. Added rp_context regression tests (Origin-first, port preserved, IPv6 bracketed, malformed->Host fallback, userinfo stripped). |
||
|
|
289b539bc4 |
harden(passkeys): accept only a well-formed http(s) Origin for RPID derivation
Follow-up on #6772. Preferring Origin over Host for the WebAuthn RPID is correct (the RPID must match the page origin, not the backend's), and it is not a trust decision: the authenticator only releases a credential whose RPID is a registrable suffix of the real page origin, clientDataJSON.origin must equal the stored origin, rpIdHash must match the stored RPID, and the assertion signature is verified against the stored credential public key. Still, only accept a syntactically valid http(s) Origin carrying a hostname, and rebuild the stored origin from the parsed scheme/host/port instead of echoing the raw header — so a malformed, non-http (javascript:/file:), or userinfo-bearing Origin falls through to the Host-derived path rather than being stored verbatim. |
||
|
|
0f76602790 | Merge PR #7331 | ||
|
|
1e00ef131b | Merge PR #6772 | ||
|
|
94b70ce8b8 | Merge PR #7385 | ||
|
|
bee7b8a9e4 |
fix(state-sync): repoint to renamed agent SessionDB API + fix agent-coupled tests
The Hermes Agent's state-module refactor (agent commit 53db597201, released v2026.9.7) renamed SessionDB.set_auto_title_if_empty -> set_auto_title( session_id, title, *, source) and (agent commit 14791b4d4e) dropped the tools.approval._ApprovalEntry back-compat facade (class moved, unchanged, to tools.approval_gateway_wait). Production impact (real, silent): api.state_sync.sync_session_title called the removed set_auto_title_if_empty, whose AttributeError was swallowed by the surrounding `except Exception: logger.debug(...)`, so WebUI auto-generated session titles stopped persisting to state.db on agent >= v2026.9.7 — hermes sessions list went blank again (the exact #6892 regression). Now calls set_auto_title with LLM provenance, which preserves the identical semantics (only fills NULL/lower-authority title, returns False untouched on a manual rename, ValueError -> get_next_title_in_lineage #N de-dup). Test fixes (agent-coupled; skip on GitHub CI which installs no agent, so they only surface on an agent-equipped box like our gate): 1. tools.approval._ApprovalEntry -> idempotent autouse conftest compat shim backfilling the class from its post-split location only when the installed agent lacks the dropped facade. No production code imports it. 2. test_goal_command_webui::test_profile_goal_falls_back_when_session_db_path_is_frozen was missing the monkeypatch.syspath_prepend(...) that every sibling test in the file has, so its bare `import hermes_state` failed under full-suite ordering after a predecessor stripped the agent dir from sys.path. Added it. Verified: authoritative 5-shard CI-equivalent split all green (0 failed); each failure proven pre-existing on clean master (not this diff). |
||
|
|
da5703d501 |
fix(agent-v0.21): port out-of-band normalizers to the transport API
hermes-agent v0.21 removed agent.anthropic_adapter.normalize_anthropic_response and the AIAgent._normalize_codex_response method; the sanctioned out-of-band contract is now agent._get_transport(<mode>).normalize_response(...) returning NormalizedResponse (.content). Port the anthropic_messages and codex_responses branches of generate_title_raw_via_agent (api/streaming.py) and _agent_text_completion (api/routes.py); on agent v0.21 those branches currently die on import/attribute errors and silently fall back, degrading title generation and handoff summaries. build_anthropic_kwargs, _anthropic_messages_create, and _run_codex_stream are unchanged in v0.21 and stay as-is. |
||
|
|
ec4bac9fc9 | fix: model picker search matches OpenRouter display names (#7228) | ||
|
|
c6a2c1b7d2 |
perf(streaming): fold the echo search window once instead of per cut index
`_strip_compact_echo_suffix` probed every candidate cut index and re-folded the whole remaining tail for each probe, so the cost grew with `window x suffix` rather than with the data actually inspected. It runs up to four times per interim assistant message, on the reasoning buffer, the shared stream mirror and up to two per-message segments. The practical effect is worst on the longest message of a turn: 297ms for a 200-character message but 5.6s for a 6000-character one, measured on the live process. That time is spent holding the GIL, so a single long final message also stalls every other stream served by the same worker. Profiling a live turn attributed ~5% of total CPU to this one helper. Fold the window once, then walk backwards across exactly as many non-space characters as the folded suffix holds. `str.isspace` and the `\s` pattern used by `_compact_for_echo_compare` agree on all 1114112 Unicode code points, so the folded view and the walk stay consistent. Behaviour is unchanged, including the leftmost-cut tie-break on repeated text and the bounded search window. The new test pins equivalence against a naive reference oracle over ~2020 cases (exact, partial and absent echoes, mixed whitespace, unicode, empty and None inputs, repetitions, out-of-window echoes), asserts the fold no longer runs once per cut index, and caps the runtime of a long-conclusion call. Measured: 200 chars 297ms -> 0.149ms, 6000 chars 5645ms -> 0.720ms. |
||
|
|
bb5347e1cd |
fix: accept pipe-separated trusted-groups header values
Authentik's proxy provider on this deployment joins multiple group names with "|" instead of "," (e.g. "admins|developpeur"). The comma-only split in _trusted_groups_header_value() treated that whole string as a single unmatched group name, silently dropping the session to the unbound "default" profile even for identities that are legitimate members of a mapped group. Single-group identities were never affected since there was no separator to parse. Accept comma, pipe, and newline as equivalent separators so the parser works regardless of which the outpost happens to send. |
||
|
|
b36fbdd633 | fix(regeneration): refuse bounded tail on in-tail duplicate keys + real WAL interleaving test (#6826 r5) | ||
|
|
7abd4773ca | fix(regeneration): snapshot reuses canonical projection+ordering; WAL data_version guard (#6826 r4) | ||
|
|
b33aff9d09 | fix(regeneration): occurrence-count guard + single-snapshot (no TOCTOU) for bounded tail read (#6826 r3 round-3) | ||
|
|
40ff74b6fc | fix(regeneration): bounded tail read only when skipped state.db prefix is provably identical; compression-anchor coverage (#6826 r3) | ||
|
|
3672e0038b | fix(regeneration): bounded sidecar-anchored tail read instead of full transcript refetch (#6826) | ||
|
|
8c9f505fab | fix(ui): avoid full transcript fetch on regenerate (#6826) | ||
|
|
037a35abfd |
perf(sessions): batch sidebar subagent source lookup (#7231)
Reuse the authoritative batched state.db sidebar metadata to classify delegated subagents before route filtering. Keep direct lookups for single-session recovery and mutation guards. Assisted-by: Hermes Agent:gpt-5.6-sol Assisted-by: Codex:gpt-5.6-luna Co-authored-by: nesquena-hermes <nesquena+hermes@gmail.com> |
||
|
|
78b0425902 |
fix(sessions): reconcile native image message mirrors (#7230)
* fix(sessions): reconcile native image message mirrors Keep the rich WebUI sidecar row when Hermes Agent stores the same exact-timestamp native image turn as scalar screenshot-marker text. Assisted-by: Hermes Agent:gpt-5.6-sol Assisted-by: Codex:gpt-5.6-luna Assisted-by: OpenCode:opencode/x-preview-f-free Assisted-by: Claude Code:claude-opus-5 * docs(replay): document native image mirror contract Record the exact cross-store identity requirements and fail-closed behavior for native image session reconciliation. Assisted-by: Hermes Agent:gpt-5.6-sol * fix(sessions): fail closed on state mirror identity gaps Treat present-versus-absent candidate identity fields as ambiguous so session restore preserves distinct state-only turns and their metadata. Assisted-by: Hermes Agent:gpt-5.6-sol Assisted-by: Codex:gpt-5.6-luna |
||
|
|
00e4c5af6f |
perf(api): skip state load on cached transcript reopen (#7212)
* perf(session-load): memoize the compression-lineage display stitch _webui_sidecar_lineage_messages_for_display re-merged multi-thousand-row parent+child transcripts on EVERY GET /api/session (measured 5.1s per request on a real 5.2k-message lineage, py-spy: merge_session_messages_ append_only + _matching_visible_duplicate dominate). The merge result is now cached per session id, validity keyed by the exact stat signature of the child sidecar AND every snapshot parent involved, bounded LRU(16). Any write to any involved sidecar invalidates naturally; cache hits and the populating call hand out shallow-copied rows so caller-side display metadata cannot corrupt cached state. Measured: 5.17s cold -> 0.002s warm on the real session; invalidation after touch re-merges. Tests: tests/test_lineage_display_cache.py (correctness, copy isolation, child/parent invalidation, no pollution for lineage-less sessions). * perf(api): memoize inactive-session display merges * perf(api): skip state load on cached transcript reopen * test(api): remove unused lineage-cache import * fix(api): reject streaming key without state database * test(api): isolate display cache fixture * fix(api): bind cache publication to read signature * fix(api): harden display cache invalidation * fix(api): bind cache hits to lineage provenance * fix(api): close remaining display cache scope races * fix(api): scope display cache revision to target * fix(api): evict unverifiable lineage provenance * fix(api): keep incomplete lineage provenance uncached --------- Co-authored-by: Anthony Ruiz <ruizanthony@users.noreply.github.com> |
||
|
|
05ab4173cf |
fix(profiles): resolve @provider:model through the shared parser (#7183)
_split_webui_provider_model_value() and _strip_webui_provider_prefix() split provider-qualified model values at the last colon. That amputates any model name carrying its own tag: @ollama:qwen3.8:27b-mtp-q8_0 resolved to model 27b-mtp-q8_0 with provider ollama:qwen3.8, and the truncated name was sent upstream, which 404'd. The splitter also cannot distinguish a tagged model from a multi-segment custom provider ID, so @custom:mykey:model-a:free was affected the same way. Both sites now delegate to config._parse_provider_qualified_model_id(), the grammar routes.py and the gateway adopted in #6723. The import is function-local, matching the existing api.config imports in this module. _split_webui_provider_model_value() persists its result into profile config, so the mangled name outlived the session that produced it. Fixes #7182. Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: nesquena-hermes <nesquena+hermes@gmail.com> |
||
|
|
c27f3d67a8 |
fix(compression): fence durable transcript revisions (#6554)
Co-authored-by: Anthony Ruiz <ruizanthony@users.noreply.github.com> Co-authored-by: nesquena-hermes <nesquena+hermes@gmail.com> |
||
|
|
eee72f942e |
fix(workspace): new chat inherits the current conversation's workspace (#7180)
* fix(workspace): new chat inherits the current conversation's workspace
`newSession()` resolved the workspace of a new chat as:
switchWs || S._profileDefaultWorkspace || S.session.workspace
`S._profileDefaultWorkspace` is global to the profile, hydrated from
`/api/profile/active`. Preferring it over the current conversation meant a new
chat opened from a MES conversation landed on the profile-global value instead
of MES. With several conversations running in parallel on different workspaces,
the context the user was actually in was systematically lost.
The precedence is worse than it looks, because that "profile default" is not a
stable configured value: `get_profile_default_workspace()` reads
`last_workspace.txt` BEFORE the configured key (api/workspace.py ~419-427), and
for the `default` profile that file is the GLOBAL one. `set_last_workspace()`
rewrites it on every `/api/chat/start` and `/api/session/update` of ANY
conversation. So "profile default wins over the current session" degrades in
practice to "whatever workspace another conversation last used wins over the
conversation I am in" -- observed live: the pointer flipped mid-operation
because a concurrent turn wrote to it.
New precedence:
switchWs || S.session.workspace || S._profileDefaultWorkspace
Preserved invariants:
- the one-shot profile-switch flag still wins first and is still consumed (#823);
- a blank page (no loaded session) still falls back to the profile default,
which remains required for the empty-composer workspace chip (#804/#5169);
- `_profileDefaultWorkspace` is never nulled by `newSession()` (#823).
This deliberately diverges from upstream #4755, which made the profile default
win. That decision assumed a stable configured default; on a shared/global
`last_workspace.txt` it inverts its own intent. The upstream test is rewritten
with the rationale inline so a future rebase does not silently restore the bug,
plus a new test that evaluates the real expression under node and proves the
blank-page fallback still holds.
Pre-existing failures in tests/test_workspace_stale_recovery.py (3) were
verified identical with and without this change.
* fix(workspace): recover stale inherited workspace
---------
Co-authored-by: Anthony Ruiz <ruizanthony@users.noreply.github.com>
|
||
|
|
0e5a0fadae |
perf(streaming): skip redundant sidecar rewrites during a turn (#7172)
The periodic checkpoint thread fires whenever a tool call completes and unconditionally rewrites the whole session sidecar. A completed tool call is not proof that anything the checkpoint persists actually changed: `run_conversation()` mutates an internal copy of the transcript, and the turn bookkeeping fields (`active_stream_id`, `pending_user_message`, `pending_started_at`) are written once before the run starts. So the recurring rewrite normally re-persists byte-identical state. Each rewrite serializes the entire session while holding the GIL, so its cost scales with transcript size. Because the HTTP server and the agent workers share one interpreter, that cost is added latency for every concurrent request — not just for the session being written. Measured on this deployment (1,622 sidecars, 6.65 GB, median 2.31 MB, 28.8% above 5 MB, max 35.9 MB): sidecar write per 10-min turn (40 wakeups) 1.8 MB ~100 ms 4.1 s -> 0.2 s 24.7 MB ~1.4 s 41.6 s -> 2.1 s A checkpoint now writes only when the state it persists actually moved, compared through a cheap fingerprint that allocates nothing. The fingerprint covers every field the checkpoint can legitimately advance mid-run: session identity (compression rotates the SID), turn bookkeeping, and the length of the persisted message projections. Fail-closed on both sides: an unreadable fingerprint returns None and forces the write, and a 5-minute idle refresh keeps `updated_at` from going stale on a long quiet turn (the sidebar sorts on it). Crash recovery is unchanged — any real mutation is still persisted within one 15 s interval. Co-authored-by: Anthony Ruiz <ruizanthony@users.noreply.github.com> |
||
|
|
0b5d2f3698 |
fix(clarify): honor agent.clarify_timeout and support unlimited wait (#7163)
The WebUI clarify bridge resolved the prompt timeout from a local, divergent copy (legacy clarify.timeout, 120s fallback) and treated 0 as invalid, so agent.clarify_timeout: 0 (unlimited) could not be honored and there was no way to make a clarification card wait for the user. Resolve the timeout with the agent core's canonical resolve_clarify_timeout() (clarify.timeout -> agent.clarify_timeout -> 3600, <= 0 = unlimited) against the session's own profile config (the streaming worker thread does not inherit the request's per-profile context), wait with a null deadline when unlimited, and stop the frontend rendering an expired countdown for non-positive timeouts. - api/streaming.py: per-session profile config resolution, canonical resolver with a compatibility fallback, and an extracted _await_clarify_response() helper that polls in 1s slices and honours cancellation. - api/clarify.py: preserve timeout_seconds <= 0 and emit no expires_at. - static/messages.js: _clarifyExpiryMs() returns 0 for non-positive timeouts so the card stays up without a countdown. - tests/test_sprint42.py: resolution matrix, per-session profile divergence, unlimited resolve-later / cancel-exit, and no-expiry payload. Closes #7159 Co-authored-by: nesquena-hermes <nesquena+hermes@gmail.com> |
||
|
|
4528279cb2 |
fix: route overlapping custom_providers models by active endpoint (#7107)
* fix: route overlapping custom_providers models by active endpoint When two custom_providers[] entries list the same bare model id (e.g. dogapi and packyapi both advertising 'claude-sonnet-5'), resolve_model_provider scanned them in config write order and returned the first match, silently ignoring the active provider's base_url. A user whose model.base_url pointed at packyapi got routed to dogapi because dogapi appeared earlier in config. Now, when the active provider resolves to a named custom provider (custom:<slug>, including via model.base_url -> named-slug matching), that provider claims models it owns before the ordered scan runs. Falls through to the legacy first-match scan when the active provider is bare 'custom' with no base_url to disambiguate, so existing single-endpoint setups are unaffected. Adds 3 regression tests covering: shared-model active-endpoint wins, provider-unique model still routes by ownership, and bare-custom no-base_url preserves legacy order. * fix: extend active-endpoint routing to aux, handoff, and slug-collision paths Review follow-up: the overlapping-id fix must reach three sibling resolution paths that still routed on the wrong endpoint. 1. Auxiliary slot base_url (config.py set_auxiliary_model): a bare resolve_model_provider(model) ignored the SELECTED aux provider, so a custom:A slot was persisted with the active custom:B endpoint when both listed the model. Look up the selected provider's own custom entry via resolve_custom_provider_connection. (NOT model_with_provider_context here: the @custom:<slug>:model form resolves base_url to None, which would drop the base_url entirely.) 2. Handoff summary (routes.py): read s_obj.model_provider and pass model_with_provider_context(model, model_provider) so a session pinned to custom:A is not rerouted to the active custom:B. base_url is backfilled from the provider's own custom entry downstream. 3. Normalized-name slug collision (config.py resolve_model_provider): two distinct provider names can normalize to the same slug (e.g. "Foo Bar" and "foo-bar"). The active-slug guard now matches the EXACT entry by base_url instead of re-deriving it from the non-unique slug, and fails closed (falls through to the ordered scan) on genuine ambiguity. Adds 4 regression tests: normalized-slug base_url disambiguation, slug-collision fail-closed, aux-slot selected-provider base_url, and session-provider-context handoff routing. * fix: fail closed on custom-provider slug collisions and break aux deadlock resolve_model_provider() returns a provider slug (custom:<slug>) and the credential lookup (resolve_custom_provider_connection) later resolves the API key from the FIRST same-slug entry regardless of base_url. So when two distinct provider names normalize to the same slug (e.g. "Foo Bar" and "foo-bar") and both list the requested model, returning the slug silently pairs one entry's endpoint with another entry's credential. Now raise a clear AmbiguousCustomProviderError instead of guessing, on both the active-slug guard and the ordered-scan fall-through paths. Also: an explicit, unambiguous named custom provider now stays authoritative even when model.base_url is stale/absent and points at neither entry — a stale URL is no longer evidence to demote it to config order. And fix a self-deadlock in set_auxiliary_model(): it held the non-reentrant _cfg_lock while calling resolve_custom_provider_connection() -> get_config() -> reload_config_if_stale(), which re-acquires the same lock. The selected custom provider's base_url is now resolved inline from the config_data already loaded under the lock. Tests: collision cases now assert the ambiguity error; added a stale-URL explicit-provider regression and a watchdog-guarded aux no-deadlock regression (both fail before the fix). Co-authored-by: Cursor <cursoragent@cursor.com> * fix: fail closed on custom-provider slug collisions across all four paths The previous cut only guarded the symmetric bare-model case, and it built collision membership from entries that OWN the requested model. But credential resolution scans ALL same-slug entries and first-matches regardless of ownership, so several slug-only boundaries could still pair one entry's endpoint with another entry's credential — including the asymmetric case where only one colliding entry lists the model. Introduce one shared, pure/lock-safe helper (_custom_provider_slug_key + _unique_custom_provider_entry) that groups ALL named custom_providers entries by a single canonical slug normalization and raises AmbiguousCustomProviderError whenever a consumed slug maps to >=2 entries. Apply it at every slug-only boundary so endpoint and credential can never come from different entries: - bare resolution (resolve_model_provider): all-entry membership for both the active-slug guard and the ordered ownership scan (was owner-only). - provider-qualified @custom:<slug>:model return (the session/send/handoff model_with_provider_context shape), which previously bypassed the check. - credential lookup (resolve_custom_provider_connection), replacing its private first-match-by-slug scan and its duplicate _slugify. - auxiliary persistence (set_auxiliary_model), replacing its own inline first-match-by-slug; the ambiguity now propagates (fails the save) instead of being swallowed, while other best-effort errors stay non-fatal. Still resolves from the in-scope config_data so it remains lock-safe (no deadlock regression). Tests: add fail-closed regressions for the asymmetric A-first-non-owner/B-owner bare case (active-endpoint and bare-custom paths), the model_with_provider_context qualified handoff shape, the credential boundary called directly, and the auxiliary-save ambiguity (asserts nothing is persisted). All five fail before this change and pass after. Co-authored-by: Cursor <cursoragent@cursor.com> * fix: one canonical custom-provider slug identity, point-of-return ambiguity, profile-scoped credentials Deep re-gate follow-up (5 findings): 1. Collision detection now derives from the single slug PRODUCER (_custom_provider_slug_from_name) instead of a looser private key, so producer-level collisions on punctuated/parenthesized names (e.g. 'Foo (Bar)' vs 'foo-bar', both -> custom:foo-bar) are detected. Previously the looser key normalized 'Foo (Bar)' to 'foo-(bar)' and missed the collision, reintroducing the endpoint-A/credential-B bug for those names. 2. The all-entry ambiguity check now runs ONLY at the point a custom:<slug> is about to be returned (via a _finalize() guard covering every custom-return in resolve_model_provider, including the final active-provider fallback), not up front. An unrelated collision on the active slug no longer blocks lanes that resolve to a different provider (@openrouter, a non-colliding @custom:other). 3. streaming.py now resolves endpoint AND credential inside ONE profile scope: the main path extends the scope over runtime-key + custom-provider resolution, and both self-heal retry paths bind the session profile. This stops a detached worker from pairing a named profile's endpoint with the default profile's key. 4. Handoff summary: AmbiguousCustomProviderError now returns HTTP 400 with the actionable rename message instead of degrading to a 200 local fallback the UI treats as success. 5. UI: aux + default model saves surface the server's message and abort (retain dirty state) instead of swallowing it into a generic failure / "settings saved"; the handoff catch shows the 400 rename message verbatim. AmbiguousCustomProviderError gains a .message attribute for verbatim forwarding. Tests: parenthesized-name collision fails closed on all direct paths; unrelated lanes are not blocked by an active collision; custom overrides resolve inside the passed profile scope; handoff returns 400 (not a persisted fallback); JS error-surfacing source guards. Verified the finding #1/#2 cases fail before and pass after (old key misses 'Foo (Bar)'/'foo-bar'; old up-front check over-blocks). Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: nesquena-hermes <nesquena+hermes@gmail.com> |
||
|
|
43bcdcf97f |
fix(transcript): recognize Agent _row_id during replay merge (#7133)
* fix(transcript): recognize agent row ids in replay merge * Strip imported _row_id so replay merge cannot trust caller provenance. Session import already dropped _state_db_row_id aliases. After treating Agent _row_id as identity, the same public/import scrubber must drop that field too, or a crafted import can collide with a later state.db turn. --------- Co-authored-by: Anthony Ruiz <ruizanthony@users.noreply.github.com> Co-authored-by: nesquena-hermes <nesquena+hermes@gmail.com> |
||
|
|
7b38e9aceb |
fix(workspace): preserve remote POSIX paths without macOS synthetic resolution (#7132)
* fix(workspace): preserve remote POSIX paths without macOS synthetic resolution (#7131) When hermes-webui runs on macOS with a remote terminal profile pointing to a Linux host (e.g. terminal.cwd='/home/<user>'), path resolution previously invoked Path.resolve(), expanding '/home' via macOS synthetic firmlinks to '/System/Volumes/Data/home/<user>' and failing validation. - Return Path(normalized_raw) directly from _remote_terminal_workspace_candidate - Check _remote_terminal_workspace_candidate in _clean_workspace_list before local _safe_resolve - Add unit test covering Linux /home/<user> remote paths without macOS expansion * ci: trigger required checks (workflow did not run on fork head) --------- Co-authored-by: n <a@n> |
||
|
|
4d03578192 |
fix(#6611): make regeneration start atomic (#6677)
* fix(#6611): make regeneration transaction canonical * fix(#6611): close cross-provider regeneration blockers * fix(#6611): harden regeneration transaction boundaries * fix(#6611): bind canonical context and fork lineage * fix(#6611): restore active turn token stamping * test(#6611): make regeneration regressions CI portable * test(#6611): preserve the tracked regeneration error fixture * fix(#6611): preserve extracted session harness contracts * fix(regeneration): preserve concurrent and imported turn ownership (#6611) * fix(regeneration): honor imported turn ownership safely (#6611) * test(regeneration): cover both session lock outcomes (#6611) * test(regeneration): clear unused transaction fixtures (#6611) * fix(#6611): consume public active-turn marker * ci: re-trigger (Playwright install hung ~2.5h on GH runners, warm-cache retry) --------- Co-authored-by: nesquena-hermes <nesquena+hermes@gmail.com> Co-authored-by: n <a@n> |
||
|
|
af36f409b0 |
perf: bound fuzzy matching for large sessions (#7128)
Co-authored-by: Dr. Carlos Daniel Carvalho <292283903+Dandandad@users.noreply.github.com> Co-authored-by: nesquena-hermes <nesquena+hermes@gmail.com> |
||
|
|
20123b1268 | fix(skills): decode JSON-array string skills.disabled in panel read and toggle write (#7120) (#7134) | ||
|
|
1768826e15 |
Keep static-serving path validator strict; scope dotfile allowance to archive members
The prior change loosened the shared _is_safe_relative_path() to permit dot-prefixed segments. That validator also gates static serving, asset URLs and manifest paths, so loosening it made hidden files reachable: GET /extensions/<id>/.env, /.git/config and /.git/hooks/pre-commit return 200 — credential exposure plus a git-hook write/execute vector. Restore _is_safe_relative_path() to strict (reject every dot-prefixed segment) and add a separate _is_safe_archive_member() wired only into the install/uninstall loops. It permits a narrow allowlist of benign hidden LEAF files (.gitkeep, .gitignore, .gitattributes, .env.example) so extensions can still ship them, while dot-directories (.git/) and all other dotfiles (.env, .secret) stay rejected. Adds regression coverage for both validators: traversal, NUL/backslash, dot-directories, and the leaf allowlist edges. |
||
|
|
63a562f6ed |
Release — bound concurrent full-session resolution (#7127) (#7130)
* perf: bound concurrent full-session resolution * Release — bound concurrent full-session resolution (#7127) * ci: re-trigger fresh run (Playwright-install hang on prior run) * ci: re-trigger (Playwright-install CDN hang on 3 shards, 12/15 passed) * ci: re-trigger (sustained Playwright-CDN hang, run 4) * ci: supersede 2h20m-zombie Playwright-hung run (run 6) --------- Co-authored-by: Dr. Carlos Daniel Carvalho <292283903+Dandandad@users.noreply.github.com> Co-authored-by: n <a@n> |
||
|
|
dcb515611d |
Release — SSE Last-Event-ID resumption fix (#6886) (#7129)
* fix(sse): scope gateway SSE probe and honor Last-Event-ID on /api/chat/stream Two narrowly-scoped cross-client SSE fixes from the hermes-android#58 follow-up: 1. The /api/sessions/gateway/stream?probe=1 response only describes the optional gateway/agent-sessions stream, but its payload gave clients no way to tell that. A 404 'agent sessions not enabled' result could be misread as SSE being unsupported server-wide, even though the persistent per-session stream (/api/session/stream) is always on and independent of the show_cli_sessions setting. Add additive scope markers to the probe payload (scope, session_stream_available, session_stream_path) and clarify the handler docstring. 2. /api/chat/stream emitted id: stream_id:seq on every journaled event but ignored the Last-Event-ID request header, so any spec-compliant SSE client that auto-reconnects (browser EventSource, Android/CLI clients) replayed the journal from seq 0 and double-rendered the transcript. _chat_stream_resume_after_seq() now resolves the resume cursor with explicit after_event_id/after_seq query params first and the Last-Event-ID header as fallback — the same resolution chain api/kanban_bridge.py already uses. Both the dead-stream journal replay and the live-subscribe gap check consume it; direct callers of the gap check that only pass query params keep historical behavior. Also documents the cross-client SSE endpoint contract in docs/sse-streams.md. Related hermes-webui/hermes-android#58 * fix(sse): split cursor presence from validity on /api/chat/stream resume Address PR #6886 review (gate-fail): the resume logic conflated "no cursor" with "invalid cursor" into a single None, which broke replay on three edges. Root cause and fix: Cursor presence vs validity — _chat_stream_resume_cursor() now returns (after_seq, resume_requested) instead of a bare seq. A client that SUPPLIED any cursor (query params, replay=1, or Last-Event-ID) asked to resume; an invalid / foreign-run / unparseable cursor resolves to after_seq=None but keeps resume_requested=True, and is normalized to replay-from-start so no journal events are silently skipped. A genuinely cursor-less request stays a fresh subscribe. 1. Explicit-query precedence is now decided by parameter PRESENCE, not successful parsing: a malformed/foreign after_event_id/after_seq never falls through to let the Last-Event-ID header override it and skip events. 2. Invalid/foreign cursors no longer bypass replay + gap detection and silently drain a truncated buffer; an ahead-of-stream cursor is no longer clamped onto the snapshot's terminal fence (which emptied the dead-stream replay by filtering the terminal frame). 3. The runner-local observe path now receives the resolved header cursor (retaining explicit-query precedence) so header-only runner reconnects resume instead of duplicating events from the start. Regression tests cover invalid-cursor replay-from-start, ahead-of-stream terminal survival, header-only runner reconnect, and explicit-query precedence; the stale clamp test is updated to the corrected contract. Related hermes-webui/hermes-android#58 * fix(sse): consume resolved resume cursor at every consumer on /api/chat/stream Address PR #6886 r2 review (gate-fail). The r1 presence/validity split fixed the entry resolution, but the individual consumers still re-derived ahead/equality/re-parse decisions, so the invariant leaked at the edges. This makes every consumer honor the resolved (after_seq, resume_requested) pair and normalizes ahead-of-stream in one place. 1. Equality off-by-one: a valid cursor EQUAL to the snapshot cutoff now enters the drain dedup bound (the event at the cursor was already delivered), instead of being treated as "ahead" and double-sending the retained buffer. Ahead is now strictly after_seq > snapshot_cutoff_seq. 2. Numeric ahead-of-stream cursors are normalized to replay-from-start: on the active path, after_seq > snapshot_cutoff_seq resets to 0 before gap-coverage/replay so a truncated buffer can no longer falsely declare coverage and lose the pre-tail events; on the dead-stream path the cursor is normalized against the journal summary's authoritative last_seq before _replay_run_journal, so an ahead cursor returns the full replay instead of an empty SSE body. 3. Runner-local now consumes the resolved cursor instead of re-parsing after_event_id or re-reading Last-Event-ID. The resolver also returns the opaque raw cursor so the runner uses the already-precedence-checked value; explicit opaque cursor= keeps precedence, invalid/foreign cursors pass no cursor (full replay), and the header is honored only when no explicit cursor was supplied. Regression tests cover the equality dedup, active+dead ahead-of-stream normalization (with after_seq-filtering mocks that assert the real replay body), and the three runner precedence probes (malformed-explicit blocks header, foreign passes no cursor, opaque cursor= wins). Related hermes-webui/hermes-android#58 * fix(sse): carry runner-cursor provenance through /api/chat/stream resume Address PR #6886 r3 review (gate-fail). The (after_seq, resume_requested, raw_cursor) tuple carried the cursor VALUE but not its PROVENANCE, so the runner handoff couldn't distinguish a valid opaque runner cursor from a journal-shaped one from a malformed one — and gated the runner handoff on journal-specific validity, which is wrong for the runner adapter (its cursors are opaque). The resolver now returns a distinct, centrally-resolved runner cursor: - a valid after_seq PAIRS with whatever after_event_id shape was supplied — including an opaque runner id like event:2 that the journal parser reads as a foreign run — so the paired runner cursor resumes at the seq (never None / full replay, which would duplicate tokens and tool events). after_seq is read directly (via _parse_run_journal_after_seq_value) rather than through the after_event_id-first journal parser, which would swallow a paired opaque runner id. - a header-only opaque runner id (Last-Event-ID: event:2) is preserved as-is so the runner resumes from it. - a malformed or foreign explicit cursor WITHOUT a valid paired after_seq still yields no runner cursor, blocking Last-Event-ID and replaying from start — the r2 malformed-blocks-header guarantee is unchanged. Regression tests pin the paired opaque id, header-only opaque id, and malformed-with-valid-seq runner resumes, plus the unchanged malformed-without-pair replay-from-start behavior. Related hermes-webui/hermes-android#58 * fix(sse): normalize ahead cursor when the snapshot cutoff is unknown Address PR #6886 r4 review (gate-fail). The ahead-cursor normalization was guarded by `snapshot_cutoff_seq is not None`, so when the subscribe snapshot had no parseable last_event_id (channel has not seen an id-bearing frame yet), a positive ahead cursor (e.g. Last-Event-ID run_1:999) was never normalized — it was installed verbatim as the live dedup bound, filtering EVERY queued frame including the terminal stream_end fence. The reconnect stalled on heartbeats with an empty SSE body. origin/master ignored the header on this path and delivered the events, so this was a new regression. Fix: treat an unknown, non-truncated snapshot cutoff as fence 0 before the ahead-cursor normalization, so a positive client cursor can't become the live dedup bound with no cutoff to bound it — it fails closed to replay-from-start, delivering the buffered events rather than silently losing them. Frames dropped while the cutoff is unknown still cannot prove gap coverage (cutoff None → not covered), so the recovery_control fail-closed path is preserved. Regression test: Last-Event-ID run_1:999 + snapshot_cutoff_seq=None delivers queued events run_1:1 and run_1:2 exactly once, emits the terminal stream_end, and releases the subscriber. Without the fix the test stalls on heartbeats (pytest-timeout kills it), proving the stall. Related hermes-webui/hermes-android#58 * Release — SSE Last-Event-ID resumption fix (#6886) --------- Co-authored-by: Paladin173 <35980893+Paladin173@users.noreply.github.com> Co-authored-by: n <a@n> |
||
|
|
59023b784c |
Release — approval no-run mirror masking fix (#7086) (#7125)
* fix(approval): stamp mirror token when creating no-run legacy mirror (#6008 follow-up) In in-process (legacy) mode, a guarded command parks an _ApprovalEntry in _gateway_queues whose .data has no run_id. submit_gateway_pending_mirror() mirrors it into _pending for the UI in the `elif not exact_local_entry` branch, but that branch never stamped _GATEWAY_MIRROR_TOKEN on the mirror. _resolve_approval_legacy() binds a no-run mirror back to the parked _ApprovalEntry via a token match (pending_token == gateway_token). Without the mirror token the match fails, resolve_gateway_pending_local() is never called and entry.event.set() never fires, so the agent thread stays blocked even though the endpoint returns ok:true and the UI closes the card. Only a ~1.5s reconcile fallback re-creates the card with a token -> the FIRST click on "Allow once" silently does nothing. Stamp the token from the live gateway head at mirror-creation time, matching the exact-local branch, so the first click unblocks the agent. Adds a regression test reproducing the scenario. * fix(approval): resolve non-head no-run mirrors by scanning all live producers The first-click fix stamps the mirror token from the mirror's own producer. But _resolve_approval_legacy() previously matched that token only against the queue head (`_gateway_queues[sid][0]`), so a non-head mirror (multiple parked producers, #7093) could not be resolved: the head's token differs from the selected producer's, the match failed, and the agent thread stayed blocked. Scan every live producer in _gateway_queues for a matching _webui_mirror_token or approval_id, and stamp the resolved producer's approval_id before delegating to resolve_gateway_pending_local(). Registered producers keep head/tail ordering, so exact-producer isolation is preserved. Adds tests/test_issue_legacy_first_click_repro.py::test_legacy_non_head_ producer_unblocks_only_its_producer, which reproduces the non-head scenario. * fix(approval): fail closed on no-run mirror token binding (818fd2fd follow-up) submit_gateway_pending_mirror() previously fell back to "first unclaimed token" or the queue head when a no-run mirror's identity (request_id/ approval_id) didn't match any live producer. That let approving a stale/foreign mirror silently resolve a different, unrelated live producer than the one the user actually saw. Bind the token only on an explicit identity match; otherwise leave the mirror tokenless so it can't borrow another producer's slot. A tokenless orphan is still created automatically for the legitimate #7093 case (no live producer at all) via reconcile_gateway_pending_mirror_locked. Adds two adversarial regression tests (stale request_id vs. a live sibling, and an identity-less mirror with multiple producers parked) and fixes an unrelated test that omitted request_id, unlike every real _ApprovalEntry. * fix(approval): don't let an unmatched no-run mirror mask a live producer Follow-up to 63087a40. Failing closed on token binding stopped stale mirror A from executing live producer B, but left A parked in _pending: reconcile then deferred to the pre-existing no-run mirror and skipped tokenizing B, so B never surfaced as the head. The real pending approval became unactionable (409 in gateway mode, ok:true resolving nothing in local mode) while an unresolvable card sat in front of it. reconcile_gateway_pending_mirror_locked() now always tokenizes every live no-run producer and derives live_token from the authoritative head whenever _gateway_queues holds producers, so an unmatched tokenless mirror is discarded instead of suppressing the real one. Retained missing-producer mirrors survive only while no producer is live, keeping the #7093 tokenless-orphan boundary intact. Tests assert the displayed == resolved invariant: the authoritative producer is the head, responding with the stale/identity-less id fails without waking anything, and approving the displayed entry wakes exactly its own producer. Three test_gateway_approval_runs_api.py cases that encoded the old masking behavior are updated to the new invariant. * docs(approval): correct reconcile comment on which mirrors survive The comment added in 9c2ca104 claimed only the authoritative head's mirror may survive while a producer is live. That contradicts the code it documents and the non-head regression test: the `entry_token in live_local_tokens` branch keeps a mirror bound to ANY live no-run producer, which is exactly what makes a non-head mirror resolvable (test_legacy_non_head_producer_unblocks_only_its_producer). State the actual rule instead — a mirror survives only if bound to some live producer's own token, the head's via live_token or a non-head producer's via live_local_tokens — so unmatched and tokenless copies are discarded without implying non-head mirrors are dropped too. Comment only; no behavior change. * Release — approval no-run mirror masking fix (#7086) --------- Co-authored-by: VHSgunzo <wosganzo@gmail.com> Co-authored-by: n <a@n> |
||
|
|
cced4f3509 |
Release — approval-card YOLO handoff fix (#6945) (#7122)
* fix(approvals): handle gateway yolo in webui * fix(approvals): preserve concurrent yolo enables * fix(approvals): honor settled yolo state * fix(approvals): commit yolo after relay * fix(approvals): preserve gateway yolo relay state * fix(approvals): close remaining yolo race windows * fix(approvals): serialize gateway yolo handoff * fix(approvals): linearize yolo disable * fix(approvals): serialize initial yolo relay * fix(approvals): serialize card yolo handoff * fix(yolo): ignore stale card responses * fix(yolo): reject stale session generations * fix(yolo): bind responses to approval owner * fix(yolo): drain pending approvals with exact ownership * fix(approvals): show skip-all busy state * fix: close approval lifecycle races * test: update approval SSE handoff contract * test: include displayed approval owner helpers * fix: linearize yolo transition registration * Release — approval-card YOLO handoff + stale-response ownership fence (#6945) --------- Co-authored-by: dss539 <dss539@users.noreply.github.com> Co-authored-by: n <a@n> |
||
|
|
5e8937981d |
Release — suppress Windows git console windows (#6534) (#7115)
* fix: suppress console windows for git subprocess calls on Windows On Windows, every git.exe spawned by the WebUI (update checks, version detection, workspace git info, rollback inspection, worktree management) creates a visible console window. When Windows Terminal is the default terminal, each short-lived git process opens a new tab. Combined with the 60-second version-skew monitor polling /api/settings and the boot-time update check, this produces a flood of flashing terminal tabs. Root cause: subprocess.run() calls in api/updates.py, api/rollback.py, api/worktrees.py, and api/workspace.py did not pass creationflags=CREATE_NO_WINDOW on Windows. api/workspace_git.py already had the fix (#5692); these four files were missed. Backend fix: - Add _windows_hide_flags() helper to each affected module (mirrors the existing pattern in api/workspace_git.py) - Pass creationflags=_windows_hide_flags() to all git subprocess.run() calls (10 call sites across 4 files) Frontend fix (prevent timeout->retry cascades): - Increase update-check timeout from 30s/60s to 300s (5 min) - Increase /api/settings fetches from 30s to 60s - Increase /api/git-info fetch from 30s to 60s The backend git fetch to GitHub can take 15s per repo; with both WebUI and Agent repos checked sequentially plus other git calls, the total easily exceeds the old 30s default frontend timeout, triggering the api() wrapper auto-retry (3 attempts) and amplifying the git process flood. * [verified] fix: address Windows git subprocess review --------- Co-authored-by: ya1yi2fa3 <ya1yi2fa3@users.noreply.github.com> Co-authored-by: n <a@n> |
||
|
|
509cdb8dfc |
Release — carry explicit_model_pick through /goal (#6705) (#7114)
* fix(session): sync provider change mid-session (#6703) * fix(session): full chat-start parity for /goal explicit-pick (#6703) Addresses review parity details on #6703: - stamp model_explicit_pick_signature after explicit goal resolution (both kickoff paths), matching /api/chat/start #5979, so a first /goal launch after a deliberate custom-provider pick survives a cold streaming catalog instead of reverting to the profile default - consume the one-shot pending session-model marker in cmdGoal, matching the chat/start path (messages.js), so a later /goal with an unchanged dropdown isn't treated as an explicit pick * fix(session): consume pending model-pick marker only after successful kickoff (#6705) * fix(session): node-driven behavioral tests for /goal explicit-pick marker (#6703) * changelog: #6705 — carry explicit_model_pick through /goal * ci: re-trigger tests on fresh runners (prior run's shards hung on GitHub-hosted runners) * ci: re-trigger (playwright browser-install hung on GH runners again; warm cache should restore) --------- Co-authored-by: webtecnica <webtecnica@gmail.com> Co-authored-by: n <a@n> Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com> |
||
|
|
69d5647b60 |
Batch release exp-v0.52.237 — profile goal isolation (#6899) + hermes-webui entry point (#6742) (#7108)
* fix(goals): delegate profile evaluation to native manager Use Hermes' context-local home override to run profile-scoped goal operations through the native GoalManager, preserving current judge, wait, and failure semantics while retaining the explicit-DB legacy fallback. * docs(goals): describe profile ownership boundary * fix(goals): gate native profile persistence capability * feat(cli): add packaged hermes-webui CLI entry point (#6739) * fix(cli): route hermes-webui entry through bootstrap:main for wheel install (#6742) * docs(changelog): note #6899 profile goal isolation + #6742 hermes-webui entry point * test(goals): guard hermes_cli import with importorskip for CI (agent not installed) #6899's native-contract tests imported hermes_cli unconditionally, failing CI with ModuleNotFoundError. Match the repo's established importorskip pattern so they skip cleanly when the agent isn't installed and run when it is. Co-authored-by: ticketclosed-wontfix --------- Co-authored-by: Nick <202622897+ticketclosed-wontfix@users.noreply.github.com> Co-authored-by: webtecnica <webtecnica@gmail.com> Co-authored-by: n <a@n> |
||
|
|
747b5a6913 |
Batch release exp-v0.52.236 — dotted model labels (#6628) + MCP SDK pin (#6616) (#7103)
* fix(models): normalize dotted Bedrock/Vertex model IDs in labels
Split out of #6607 at reviewer request — it was unrelated to that PR's MEDIA
path handling and needed its own tests.
`us.anthropic.claude-opus-5` carries a cross-region routing prefix plus a vendor
namespace, and `mistral.mistral-large-2407-v1:0` adds a provisioned-revision
suffix. None of it belongs in a human label, so the turn footer rendered
"Us.anthropic.claude Opus 5".
Strip only the two shapes these hosts actually publish, against a CLOSED
provider allow-list:
<region>.<vendor>.<model> us.anthropic.claude-opus-5
<vendor>.<model> mistral.mistral-large-2407-v1:0
A generic "drop leading letters-only dot segments" loop was rejected because it
rewrites arbitrary uncatalogued IDs: `deepseek.v3` rendered as "V3" (vendor name
silently deleted) and `foo.bar.baz` as "BAZ". Dropping a vendor is additionally
gated on the remainder still naming the model, so `deepseek.v3` — where the
vendor IS the name — is left byte-intact.
Backend and frontend are paired: tests/test_dotted_model_label.py drives one
table through both `_get_label_for_model()` and `_stripDottedModelPrefix()` and
fails on divergence, including version dots (`gpt-4.1`, `qwen3.6-35b`),
URI-scheme IDs, and unknown vendors.
The Python half is inlined inside `_get_label_for_model` on purpose: the #3429
harnesses extract that function's source and eval it in isolation, so a
module-level helper NameErrors there. The #3429 JS driver is updated to pull in
the new helper for the same reason.
* fix(models): recognize `global` as a Bedrock routing prefix
Re-review catch, and a real gap: the catalog ships six
`global.anthropic.claude-*` IDs (api/config.py:1901-1909) and the first-party
routing notes use `global.anthropic.claude-…` as the canonical Bedrock shape,
but `global` was missing from the region allow-list. All six therefore kept the
noise this change exists to remove:
global.anthropic.claude-opus-4-7 -> "Global.anthropic.claude Opus 4 7"
Added to the region set in both implementations, which now read identically.
The root cause is two lists that must agree — the region allow-list and the
shipped catalog — so the new guard is catalog-driven rather than another
hardcoded region list: it scrapes every dotted `<head>.<vendor>.<model>` ID out
of api/config.py and asserts none of them keeps a routing/vendor namespace in
its label. A future routing prefix added to the catalog without updating the
region set fails there, instead of shipping mislabeled.
Also pins all six `global.*` IDs plus `us-gov` in the shared strip table (no
suite covered `us-gov` before either), and verified by mutation: dropping
`global` from the Python set fails 3 tests, dropping it from only the JS set
fails 2 — so backend/frontend parity drift is caught, not just a total absence.
Note: the review cited tests/test_provider_prefix_label_normalization.py and
tests/test_ui_model_label_parity.py as the suites to extend; neither exists in
this tree (nothing under tests/ referenced `us-gov` at all), so the cases live
in tests/test_dotted_model_label.py alongside the existing paired coverage.
* fix(models): add missing Bedrock vendors; make the catalog guard actually broad
Two self-review findings (hostile critic pass). Both are the same class as the
`global` gap the reviewer already caught, which means the guard I added for that
gap was too weak to prevent a recurrence.
1. Missing vendors. `luma`, `twelvelabs` and `ibm` are real Bedrock
foundation-model vendors and were absent from the allow-list, so genuine IDs
shipped with the namespace intact:
luma.ray-2 -> "Luma.ray 2"
us.twelvelabs.marengo-embed-2-7 -> "Us.twelvelabs.marengo Embed 2 7"
us.ibm.granite-3-8b -> "Us.ibm.granite 3 8B"
Added those plus `nvidia` and `snowflake` to both implementations.
2. The catalog guard inspected 6 of 75 dotted IDs. Its scrape regex only matched
three-segment `"id": "<region>.<vendor>.<model>"` literals with double quotes,
so the entire two-segment `<vendor>.<model>` shape -- the OTHER documented
shape this PR handles -- was invisible to it. A test that reads 8% of the
corpus while claiming to cover the catalog is worse than no test, because it
reads as proof.
Rewritten to scrape any quoted `id` value regardless of quote style or segment
count (75 dotted IDs now inspected), and to DERIVE the namespace heads from the
production `_regions`/`_vendors` literals instead of retyping them, so a set
that grows without a test update is still covered. Version dots (`qwen3.6-plus`,
`gpt-5.4`) are correctly skipped as non-namespaces.
Also added an explicit per-vendor round-trip test. It asserts no dotted NAMESPACE
survives rather than that the vendor word is absent, because a vendor legitimately
reappears inside some model names (`mistral.mistral-large-2407` -> "Mistral Large
2407") -- my first version of that assertion was wrong for exactly that reason.
Verified by mutation: removing the new vendors fails the round-trip test; deleting
the strip entirely fails 6 tests.
* test(models): drive real getModelLabel() in the parity oracle
The paired test compared _get_label_for_model() against itself, so JS
getModelLabel() never ran and nothing asserted the two sides agreed. It
stayed green when the JS dotted strip was reverted to a no-op AND when the
whole post-strip retry chain was deleted -- blind to both.
Replaced with two tests driven through the real getModelLabel() under Node,
reusing the driver already proven in test_issue3429_uri_scheme_model_label.
Only sinks are stubbed (_dynamicModelLabels empty as it is pre-fetch,
_fmtOllamaLabel identity); every decision function is the shipped source.
The oracle asserts the actual contract -- no routing/vendor namespace leaks
into either label -- rather than string equality of the two labels. Label
divergence is pre-existing and by design: at base
|
||
|
|
92c790ffe5 |
Batch release exp-v0.52.235 — cancelling-run reattach (#7096) + GPT-5.6 max reasoning (#7083) + test order-independence (#7101) (#7102)
* fix(tests): make mtime_invalidation and glm_5_3 tests order-independent (#7100) * fix: expose max reasoning for GPT-5.6 models * fix(recovery): do not reattach cancelling runs ACTIVE_RUNS tracks worker lifecycle, which is deliberately broader than "a turn a browser may attach to". cancel_stream() keeps the row as phase="cancelling" while the worker unwinds so a successor turn cannot start on top of it, but the client has already reached a terminal state for that stream: its run journal ends in a terminal event. The recovery lookups treated every same-session ACTIVE_RUNS row as attachable. An idle session holding a cancelling row therefore received a recovered server_turn_started on every /api/session/stream subscription: the client attached, replayed the terminal event, tore the renderer down, resubscribed, and the server replayed the same frame again. The result is an endless attach/replay loop that rebuilds the transcript repeatedly. Separate the two meanings instead of narrowing one call site: - api/config.py gains active_run_is_attachable() and active_run_cancel_is_stale() as the shared predicates. - active_stream_id_for_session() (browser recovery) returns attachable rows only; _session_has_active_turn() (busy check) keeps counting a fresh cancellation so a successor cannot overlap the unwinding worker. - _live_active_stream_id() applies the same rule to the hidden-tab status poller, on both the STREAMS and ACTIVE_RUNS paths. - routes._cancelled_run_is_stale() now delegates to the shared predicate rather than keeping a parallel copy of the staleness rule. - A cancelling row past a bounded unwind window with no live STREAMS channel is reclaimed from ACTIVE_RUNS and its stream owner released, so a wedged worker cannot suppress background wakeups forever. Age alone does not reclaim a row that still owns a live channel. Staleness anchors on cancelled_at, falling back to started_at, so a long-running turn cancelled moments ago is never treated as an orphan. tests/test_cancelling_run_not_attachable.py covers both directions of each rule. The six behavioral tests fail on the unpatched tree and pass with the fix; two targeted mutations (forcing the attachability predicate true, and disabling the staleness reaper) each turn the suite red. * docs(rfc): document cancellation attach/admission split Records the runtime contract introduced by the recovery fix in the WebUI run-state consistency RFC, so the distinction is discoverable instead of living only in code comments. - Adds ACTIVE_RUNS to the State Layers table as the worker-lifecycle registry, explicitly not the set of runs a browser may attach to. - Adds invariant 9: lifecycle-busy is not client-attachable. Cancellation splits the two meanings, recovery paths must exclude cancelling rows, and admission checks must keep counting them. - Documents the bounded cancellation-unwind window: reclamation needs both age and the absence of a live STREAMS channel, and staleness is anchored on the cancellation timestamp. - Extends the review checklist with the admission-vs-attachment question and the evidence required when changing a reclamation window. * docs(changelog): note #7096 cancelling-run reattach, #7083 GPT-5.6 max reasoning, #7101 test order-independence --------- Co-authored-by: webtecnica <webtecnica@gmail.com> Co-authored-by: Abdulrahman Elkenany <boudy.elkenany123@gmail.com> Co-authored-by: allenliang2022 <allenliang2022@users.noreply.github.com> Co-authored-by: n <a@n> |
||
|
|
e88aff4388 |
Batch release exp-v0.52.234 — O_BINARY (#7077), native-control font (#6871), mobile nav order (#6909) (#7099)
* fix(navigation): preserve mobile action mirror order Reposition stale extension action mirrors during synchronization without moving already-correct elements, preserving native tab order and keyboard focus. Assisted-by: Hermes Agent:gpt-5.6-sol Assisted-by: Codex:gpt-5.6-luna Assisted-by: Claude Code:claude-opus-5 * fix(typography): route native controls through UI font Assisted-by: Hermes Agent:gpt-5.6-sol Assisted-by: Codex:gpt-5.6-luna Assisted-by: Claude Code:claude-opus-5 * fix(windows): open anchored files in binary mode * docs(changelog): note #7077 O_BINARY, #6871 native-control font, #6909 mobile nav order --------- Co-authored-by: starship-s <45587122+starship-s@users.noreply.github.com> Co-authored-by: allenliang2022 <allenliang2022@users.noreply.github.com> Co-authored-by: n <a@n> |
||
|
|
d9c5b33449 |
fix(approval): match the notified gateway-copy to its producer by request_id (#7093)
submit_gateway_pending_mirror() matches an incoming approval back to its queued producer entry by object identity (entry.data is approval) then by approval_id. On the LOCAL backend neither holds: the installed hermes-agent core delivers the approval to WebUI's notify callback as a COPY (notify_cb(dict(entry.data))), so identity fails, and a local _ApprovalEntry stamps a request_id but no approval_id, so the approval_id fallback misses. The mirror is then created with no token, reconcile keeps the orphan, and _session_has_pending_approval stays True after the entry is dropped — the stale-approval-card dead-end (#4948 local variant): clicking a card whose approval is gone returns a bare ok:false the UI renders as "Approval response not accepted." with a stuck card. Add a request_id fallback (gated on the local not-run_id path) that reunites the notified copy with its queued entry via the request_id the core stamps once on the source and preserves through the copy. The four HTTP tests in test_approval_unblock.py were unfaithful to production: they built _ApprovalEntry(approval) (stamping request_id onto a copy in entry.data, not onto the source approval) then submitted the pre-stamp source. They now submit dict(entry.data), matching what the core actually passes. The assertions are unchanged; with the fidelity fix but the production change reverted, the tests fail — so they detect the real regression, not vacuously. This class is invisible on CI, which never installs the agent core, so both approval test files skipif-skip entirely and their green never exercised these paths. Co-authored-by: n <a@n> |
||
|
|
d13e922120 |
Release batch B: Docker single-container UID fix + GLM-5.3 catalog (experimental) (#7090)
* feat: add GLM-5.3 to Z.AI model list Add glm-5.3 as the newest zai entry in _PROVIDER_MODELS and a matching zai/glm-5.3 entry in _FALLBACK_MODELS, and bump the Z.AI onboarding default_model from glm-5.1 to glm-5.3 (Z.ai's current flagship; legacy GLM-5.2/5.1 requests are routed to GLM-5.3 per docs.z.ai). Reasoning gating needs no change: _zai_glm_classification() treats GLM >= 5.2 as effort-ladder capable, so glm-5.3 is already covered and pinned by tests/test_zai_reasoning_effort_gating.py. New regression coverage in tests/test_glm_5_3_catalog.py: catalog presence, newest-first ordering, fallback entry, onboarding default, full reasoning_effort ladder, and the get_available_models() payload. * test: restore _cfg_fingerprint in catalog test fixture Review follow-up (#7017): the isolation fixture snapshot restored cfg, _cfg_mtime, and _cfg_path but left _cfg_fingerprint pointing at the temporary config loaded by the payload test. api/config.py uses that fingerprint to distinguish in-memory overrides from changed files (config.py:371), so a stale value could make later same-process tests skip reloading a changed config. Snapshot and restore it like the rest. * fix: keep Z.AI onboarding default at glm-5.1 until direct API serves GLM-5.3 Review follow-up (#7017): GLM-5.3 is live on Z.ai's Coding Plan endpoint only; the direct api.z.ai endpoint the zai provider uses still lists the GLM-5.3 API as coming soon. Defaulting new direct-API users onto glm-5.3 would fail their first message, so the catalog addition stays (opt-in) and the default stays glm-5.1. Bump the default in a follow-up once the direct endpoint serves GLM-5.3. * fix(docker): probe the configured state dir before /workspace for UID/GID (#7027) UID/GID auto-detection probed /workspace before the configured HERMES_WEBUI_STATE_DIR. In a stock single-container image none of the priority-1 candidates exist, but /workspace does — owned by the image's build-time 1024:1024. Detection therefore returned the image's own owner, which carries no information about the host, while the one directory whose owner *is* the host UID by definition — the state-dir bind mount — was never probed. With a host-owned state mount and no explicit WANTED_UID the container remapped to 1024, failed its own state-dir writability check, and restart-looped. The log line made this expensive to debug: 1024 is also the fallback default, so "Auto-detected workspace UID: 1024" read as if detection had found nothing. - probe ${HERMES_WEBUI_STATE_DIR:-/app/data} first, for both UID and GID - keep /workspace as a lower-priority signal (unchanged for setups that actually bind-mount it), and keep the hermes-home probes from #668 - stop treating an explicitly supplied 1024 as "unset": the sentinel and a valid UID were the same number, so an operator who deliberately ran as 1024 got it overwritten by detection. The explicit/detected origin is persisted next to the value because `su` drops the environment when the script re-enters as the runtime user, so the second pass would otherwise see an explicit choice as a detected one. Tests: tests/test_7027_state_dir_uid_probe.py runs the real resolution block under bash with `stat` stubbed, covering the non-1024 state mount, the explicit 1024 override (including across the privilege drop), and the pre-existing /workspace + hermes-home fallback paths. A new state-dir-uid job in the Docker smoke workflow boots a real container on a host-owned state mount and gates on /health. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Release batch B: Docker UID fix + GLM-5.3 catalog (experimental) Two independently gate-passed contributor PRs, rebased fresh onto master and re-gated as a combined stage (Codex SAFE TO SHIP; full suite green except the 8 pre-existing approval-test failures that fail identically on clean origin/master — CI green on the same commit; tracked separately for a fix). - #7027 (@jorgejiro) probe configured state-dir before /workspace for Docker UID/GID; persist explicit marker so a supplied 1024 survives root->su re-entry (fixes the single-container restart loop) (#7034) - #7017 (@rh-id) add GLM-5.3 to the Z.AI model list (onboarding default stays glm-5.1) Co-authored-by: jorgejiro <jorgejiro@users.noreply.github.com> Co-authored-by: rh-id <rh-id@users.noreply.github.com> --------- Co-authored-by: Ruby Hartono <58564005+rh-id@users.noreply.github.com> Co-authored-by: Jorge <jorgejiro@gmail.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-authored-by: n <a@n> Co-authored-by: jorgejiro <jorgejiro@users.noreply.github.com> Co-authored-by: rh-id <rh-id@users.noreply.github.com> |
||
|
|
8686528b1b |
Release batch A: 6 low-risk gate-passed fixes (experimental) (#7089)
* fix(goal): suppress reserved SILENT sentinel at /api/goal ingress A wake relay POSTing the exact [SILENT] suppression sentinel to /api/goal lets it reach _start_chat_stream_for_session, which persists it as pending_user_message. If 8701 restarts while the turn is pending, session recovery materializes it as a visible _recovered user turn — the same phantom-recovered-turn exposure #7018 closed for chat ingress. Apply the shared _is_silent_control_message() guard immediately after /api/goal session-ID validation and before session lookup or goal-state mutation, returning the same 200 no-op. Matching stays exact and case-sensitive; ordinary kickoff text is unaffected. Add tests mirroring test_silent_control_suppression.py for the goal path (args and text fields, before-lookup suppression, exact-match semantics). Closes #7019 * fix(sessions): keep evicted subagent parents in the import window The sidebar nests a subagent row under its parent only when the parent row is present in the same payload. The visible-window limit was applied as a flat per-row recency slice, so a frozen orchestrator (which stops writing while its leaves keep streaming) lost the recency race against its own leaves and fell outside the window -- promoting those leaves to top-level sidebar rows. Re-add subagent parents that the oversampled candidate set already projected, after the slice. No extra queries, no change to CLI_VISIBLE_SESSION_LIMIT. webui ancestors are deliberately not imported because that sidebar bucket already owns them. Supersedes #7031. * fix(sessions): document and pin the parent-recovery bound (greptile review) Greptile flagged (P1) that a selected subagent child whose parent ranks below the limit * 8 oversample is still promoted to a top-level row. That is real and measured (the parent drops out at candidate #25 of 24), but it is the bound of the design, not a regression: the walk reuses rows the projection already fetched and never issues an extra query. Resolving arbitrarily old ancestors needs an unbounded per-row lookup on the hot sidebar path -- the approach rejected in #7031 -- so the bound is documented and pinned instead. - Docstring: state that `limit` bounds the recency slice, not the row count, so callers must iterate rather than assume len(rows) <= limit; state that recovery is bounded by the oversampled candidate set. - Inline comment: mark the bound at the exact lines Greptile flagged and point at `candidate_limit` as the knob if the window proves too tight. - Tests: cover the candidate-window exhaustion Greptile said was untested -- parent inside the oversample is recovered, parent beyond it stays unresolved -- plus the over-limit return contract and a parent-cycle guard. No behaviour change; 7 tests pass. * fix(share): add _hadAppearance guard to prevent fabricated appearance choice Summary: The inline appearance bootstrap in static/share.html writes hermes-theme and hermes-skin to localStorage unconditionally on every page load, even when the browser had no prior appearance state. This fabricates an explicit user choice on first access via a shared link, making the server-side SETTINGS_DEFAULTS unreachable for deployments that customise the default theme or skin. Root Cause: share.html:9 — the boot IIFE resolves a theme+skin and calls localStorage.setItem() without guarding on whether the user had previously chosen an appearance. The same bug was fixed in index.html by PR #6808 (commit tomtong2015) but share.html was left unchanged. Change: 1. Added _hadAppearance guard before the two localStorage.setItem() calls: var _hadAppearance = localStorage.getItem('hermes-theme') !== null || localStorage.getItem('hermes-skin') !== null; if (_hadAppearance) { setItem('hermes-theme', t); setItem('hermes-skin', s); } 2. The first-paint DOM mutations (classList.add('dark'), dataset.skin) remain outside the guard — only persistence is protected. 3. Synced the skin allowlist with index.html: added neon-soft and neon-paint (zeus and verdigris were already present). Verification: - test_6808_appearance_bootstrap_no_fabricated_choice.py: 11/11 passed covering fresh-browser (no writes), pre-paint fallback, explicit state normalisation, and legacy migration survival. Closes #7030 * fix(tests): pin rebuild budget in issue2513 custom-provider catalog test test (3.13, 4) failed on this PR with: assert "@custom:alpha-proxy:alpha/remote" in alpha_ids E AssertionError: assert '@custom:alpha-proxy:alpha/remote' in {'alpha/sticky'} WARNING api.config:config.py:8355 live provider-catalog rebuild exceeded 4.0s budget - serving fallback, refreshing catalog out-of-band Pre-existing wall-clock flake, not a regression from this PR: this branch touches only api/agent_sessions.py and tests/test_subagent_parent_in_import_ window.py, and both api/config.py and this test file are byte-identical to origin/master. The same shard passed on 3.11 and 3.12. The test never pinned _LIVE_REBUILD_BUDGET_SECONDS, so it raced the global 4s budget in get_available_models(). On a starved runner the cold rebuild overruns, the degraded fallback catalog is served, and the monkeypatched- urlopen model alpha/remote is dropped - leaving only the config-declared sticky model, exactly as CI observed. Force the synchronous (unbounded) rebuild path, matching the existing precedent in tests/test_issue2540_models_endpoint_error.py:20-24. Verified: with the budget forced to 0.001s the unpatched test reproduces the CI assertion verbatim; patched it passes at 0.001s, 0, and default. * fix(#7013): preserve media deny coverage across platforms * fix(docker): keep repository agents out of runtime context * fix(docker): gate image runtime proof behind integration job * test(models): stabilize custom provider catalog regression * Release batch A: 6 low-risk gate-passed fixes (experimental) Batched contributor fixes, each individually Codex-gated during the overnight certifier cycles and re-verified clean-to-ship as a combined stage (Codex SAFE TO SHIP on the combined diff; full suite green except 8 pre-existing approval tests that fail identically on clean origin/master — CI green on same commit). - #7019 (@webtecnica) suppress reserved [SILENT] sentinel at /api/goal - #7031 (@carlotestor) keep evicted subagent parents in the sidebar import window - #7030 (@webtecnica) share.html first-visit appearance guard + skin-id sync - #6853 (@rodboev) exclude repo-root AGENTS.md from the Docker runtime image - #7013 (@webtecnica) test-only: platform-neutral test portability (playwright import guard + media tests served from allowed roots) - test-only (@carlotestor) pin custom-provider catalog rebuild budget (#7054) Co-authored-by: webtecnica <webtecnica@users.noreply.github.com> Co-authored-by: carlotestor <carlotestor@users.noreply.github.com> Co-authored-by: rodboev <rodboev@users.noreply.github.com> --------- Co-authored-by: webtecnica <webtecnica@gmail.com> Co-authored-by: carlotestor <carlotestor@users.noreply.github.com> Co-authored-by: Rod Boev <rod.boev@gmail.com> Co-authored-by: n <a@n> Co-authored-by: webtecnica <webtecnica@users.noreply.github.com> Co-authored-by: rodboev <rodboev@users.noreply.github.com> |
||
|
|
6d6560f286 |
fix(chat): suppress reserved SILENT control turns at ingress (#7018)
Cron agents use the exact final response `[SILENT]` as a delivery-suppression
sentinel. If a wake relay accidentally POSTs that sentinel to `/api/chat/start`
and 8701 restarts while the turn is pending, session repair materializes it as
a visible `{role: user, _recovered: true}` message. The pending value can then
be recovered again on later restarts, creating repeated `[SILENT]` turns.
Treat the exact normalized sentinel as a successful no-op at both server-side
turn entry points: the HTTP `/api/chat/start` handler and
`start_session_turn`. Both checks run before session lookup, runtime barriers,
or pending-state mutation. Matching is deliberately exact and case-sensitive,
so `[silent]`, prose containing `[SILENT]`, and ordinary user text are not
suppressed.
Add regression tests proving both paths return HTTP/status 200 without session
lookup, plus negative cases for non-exact text. The new tests fail 3/3 before
the fix. Targeted and neighbouring chat-start suites pass 21/21.
Co-authored-by: allenliang2022 <allenliang2022@users.noreply.github.com>
Co-authored-by: nesquena-hermes <nesquena+hermes@gmail.com>
|
||
|
|
9b8a9e05d1 |
fix(sidebar): prevent kanban runs from hiding CLI sessions (#6998)
* fix: prevent kanban rows from hiding CLI sessions Keep worker-heavy Kanban history out of the bounded interactive session window and recover it through a separate capped pass. Add regression coverage and update query-pass contract tests. * docs: describe imported session projection bounds --------- Co-authored-by: zicochaos <23367003+zicochaos@users.noreply.github.com> Co-authored-by: nesquena-hermes <nesquena+hermes@gmail.com> |
||
|
|
7a5e6bc27a |
fix(providers): reduce pool-exhaustion TTL for HTTP 402 from 1h to 2min (#6626)
* fix(providers): reduce pool-exhaustion TTL for HTTP 402 from 1h to 2min Mirrors the agent-side fix: when a provider returns HTTP 402 (Payment Required), the credential pool marks the key as exhausted with a 1-hour TTL — even though recharging credits restores the key immediately. Both copies of _entry_exhausted_ttl_seconds() now handle HTTP 402 with a 2-minute TTL, matching the new EXHAUSTED_TTL_402_SECONDS constant in agent/credential_pool.py. * fix(providers): tie HTTP 402 TTL to installed runtime contract (#6626) * test(providers): behavior-test 402 TTL 119/120s boundary on both helper copies (#6626) Add executed boundary tests for the HTTP 402 pool-exhaustion TTL: - Host helper: with the runtime TTL monkeypatched to 120, an exhausted entry is still unavailable at 119s and becomes available at 120s, for both the integer 402 and the persisted string "402" error codes. - Embedded _ACCOUNT_USAGE_SUBPROCESS_CODE copy: execute the subprocess code under the existing quota-subprocess harness and assert the same TTL and 119/120s eligibility boundary against the embedded helper, instead of counting source-string occurrences. - Keep the legacy 3600s fallback and last_error_reset_at precedence assertions. * test(providers): make 402 TTL parity tests self-contained for standalone CI The parity tests imported agent.credential_pool directly, but the WebUI test suite runs standalone (Hermes Agent runtime is not installed in CI), causing ModuleNotFoundError on every shard since 08-02. Simulate the runtime contract with a fake agent.credential_pool module (same pattern already used by the embedded-subprocess helper test) so both the 120s cooldown and the legacy 1h fallback paths are covered. --------- Co-authored-by: nesquena-hermes <nesquena+hermes@gmail.com> |