Commit Graph
8475 Commits
Author SHA1 Message Date
nesquena-hermes e577777051 Merge master (exp-v0.52.355) into stage/7760-lease-warnings
# Conflicts:
#	CHANGELOG.md
2026-09-23 13:06:40 +00:00
nesquena-hermes 6f6d7a6451 Merge pull request #7773 from nesquena/stage/7761-watcher-fence
Release: #7761 gateway watcher stale-projection fence (@franksong2702)
2026-09-23 06:05:11 -07:00
nesquena-hermes fbfb737f72 Merge master (exp-v0.52.354) into stage/7761-watcher-fence
# Conflicts:
#	CHANGELOG.md
2026-09-23 12:57:23 +00:00
nesquena-hermes 5203e6a5e1 Merge pull request #7772 from nesquena/stage/7758-wakeup-budget
Release: #7758 retryable wake-up refusals keep their delivery budget (@franksong2702)
2026-09-23 05:56:18 -07:00
nesquena-hermes ae081638d6 Merge master into stage/7760-lease-warnings
# Conflicts:
#	CHANGELOG.md
2026-09-23 12:47:53 +00:00
nesquena-hermes c5d8e5fe4f Merge master into stage/7761-watcher-fence
# Conflicts:
#	CHANGELOG.md
2026-09-23 12:47:36 +00:00
nesquena-hermes 4eef3f3bbf Merge master into stage/7758-wakeup-budget
# Conflicts:
#	CHANGELOG.md
2026-09-23 12:46:45 +00:00
nesquena-hermes 2930cefc71 Merge pull request #7769 from nesquena/stage/7730-first-visit-locale
Release: #7730 first visit uses the browser locale (@happy5318)
2026-09-23 04:49:56 -07:00
nesquena-hermes f159d6d219 Merge master (exp-v0.52.352) into stage/7730-browser-locale
# Conflicts:
#	CHANGELOG.md
2026-09-23 11:41:53 +00:00
nesquena-hermes 38d0e10fe4 Merge pull request #7768 from nesquena/stage/7165-custom-allowlist
Release: #7165 custom-provider live models allowlist (@happy5318)
2026-09-23 04:40:35 -07:00
nesquena-hermes 002945c85a Merge master (exp-v0.52.351) into stage/7730-first-visit-locale 2026-09-23 11:33:37 +00:00
nesquena-hermes 00cefff681 Merge master (exp-v0.52.351) into stage/7165-custom-allowlist 2026-09-23 11:33:37 +00:00
nesquena-hermes 3f00824916 Merge pull request #7766 from nesquena/stage/7656-resolve-cap
Release: #7656 configurable full-session resolve concurrency (@happy5318)
2026-09-23 03:49:19 -07:00
nesquena-hermes 9e8426236a docs(changelog): #7760 2026-09-23 10:41:45 +00:00
nesquena-hermes 75554fd7d6 docs(changelog): #7761 2026-09-23 10:41:22 +00:00
nesquena-hermes 0327db23ef docs(changelog): #7758 2026-09-23 10:41:22 +00:00
nesquena-hermes 86b95eadb7 Merge PR #7760 into stage/7760-lease-warnings 2026-09-23 10:29:33 +00:00
nesquena-hermes ec78f8a80d Merge PR #7761 into stage/7761-watcher-fence 2026-09-23 10:29:29 +00:00
nesquena-hermes 7f4668eb1a Merge PR #7758 into stage/7758-wakeup-budget 2026-09-23 10:29:26 +00:00
nesquena-hermes c767ac70b3 docs(changelog): #7730 2026-09-23 10:23:04 +00:00
nesquena-hermes b43a3b4d43 docs(changelog): #7165 2026-09-23 10:23:03 +00:00
nesquena-hermes aebcd96ff8 Merge PR #7730 into stage/7730-first-visit-locale 2026-09-23 10:22:45 +00:00
nesquena-hermes dc554cbd1c Merge PR #7165 into stage/7165-custom-allowlist 2026-09-23 10:22:41 +00:00
nesquena-hermes 0ff718bb11 docs(changelog): #7656 resolve-concurrency setting 2026-09-23 10:16:42 +00:00
nesquena-hermes daad118675 Merge PR #7656 (@happy5318) into stage/7656-resolve-cap 2026-09-23 10:16:21 +00:00
Anthony Ruiz 986e0fdd85 fix(gateway-watcher): fence stale projections after final subscriber removal 2026-09-23 10:00:57 +00:00
Anthony Ruiz 538481c234 fix(gateway-watcher): reset the cheap fingerprint when the last subscriber leaves
_poll_once() resets the parity timestamp when it runs with no subscribers so
the next subscription gets a fresh projection, but since the idle-park change
(#7694) the poll loop parks before calling _poll_once() once the last
subscriber leaves. A tab that reconnects inside the parity interval then
resumes with the stale cheap fingerprint and skips the projection.

Reset _last_cheap_fp and _last_full_projection_at in unsubscribe() when the
last subscriber is removed (not for an unknown queue, not while others
remain).
2026-09-23 09:57:20 +00:00
happy5318andClaude Opus 4.8 0fcd584274 fix(models): dict-shaped models is per-model metadata, not an allowlist (re-gate 9/23)
Re-review #7165 (nesquena-hermes 2026-09-23T06:21:57Z, CHANGES_REQUESTED).

CORE: a dict-shaped custom_providers[].models mapping (what Hermes Agent's
setup flow writes, e.g. {chat-a: {context_length: 128000}}) was treated as an
allowlist by iterating its keys, collapsing the live /v1/models catalog to a
single saved default while the CLI live-probed the full list. The Agent defines
exactly this contract in hermes_cli/model_switch.py::_models_config_is_allowlist:
a mapping is per-model metadata, NOT a catalog narrow.

Fix (all 3 spec points):

1. api/routes.py::_custom_provider_allowlist_ids: added
   _custom_provider_models_discover_is_false(_cp) (mirrors
   api.config._provider_discover_allowed / Agent
   model_switch_providers.py:557: bool False or string false/no/0,
   case-insensitive). In the dict branch, when discovery is NOT explicitly
   false, return no allowlist so the live probe returns the full catalog.
   Only discover_models: false opts into treating dict keys as a pinned
   allowlist. List / JSON-array-string / Python-literal shapes stay plain
   allowlists.

2. tests/test_issue3718_live_models_custom_probe.py: removed the
   mapping-metadata param that asserted the regression; added two
   real-handler tests: test_dict_models_without_discover_false_does_not_gate
   (dict -> full _LIVE_CATALOG) and test_dict_models_with_discover_false_pins_to_keys
   (bool False / "false" / "no" / "0" -> pinned to keys).

3. docs/architecture/live-models-allowlist.md: documented that list and
   string shapes filter, and a dict filters only with discover_models: false.

Companion test update (blocker reported by the fix itself):

tests/test_byok_model_dropdown.py rightcode-codex entry encoded the removed
dict-as-allowlist behavior (models: {"gpt-5.5-mini": {}} asserting the response
collapsed to just that key). Added discover_models: False to preserve the
provider-scoping intent (no sibling leak) under the new contract.

Red/green verified: removing the dict-branch guard turns
test_dict_models_without_discover_false_does_not_gate red (collapsed catalog);
restoring it turns it green.

Verification: 3718 18 passed; byok 24 passed; neighbour sweep (46 files)
520 passed (2 pre-existing order-dependent flakes reproduce identically on
clean HEAD, verified via git stash); ruff curated gate OK.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-09-23 17:48:34 +08:00
happy5318 c30315747a fix(tests): scope api.models reload to test fixture; prevent cross-suite pollution (#7656 round 4)
#7656 re-gate (9/22 23:18), 1 finding (CI red root cause):

**Finding (CI RED, deterministic, was on me)**: the round-3 test
fixture used ``_import_models_with_env`` that did ``sys.modules.pop``
on ``api.models`` and re-imported to recompute the module-level cap
constant. Modules imported earlier (``api.routes``,
``api.streaming``, ...) keep references to the *old* module's
``_FULL_SESSION_RESOLVE_MAX_CONCURRENT`` / ``_FULL_SESSION_RESOLVE_SLOTS``,
so a later test would save through one module and read through
another, producing cross-suite pollution.

The maintainer reproduced this in CI's no-agent shape:

  tests/test_session_sidecar_repair.py                                              82 passed
  tests/test_issue7421_resolve_cap_config.py tests/test_session_sidecar_repair.py   9 failed, 88 passed
    FileNotFoundError: .../sessions/_run_journal/gateway_terminal_bad_session_id/...

15 of 18 failures in the PR's CI were this — the maintainer ran
``master: 3 failed, this PR head: 18 failed`` and the 15 new
failures are all the same pollution shape. I was wrong in round
3 to call it a flake.

**Fix** (maintainer-suggested, kept verbatim):

- Replace ``_import_models_with_env`` with a ``monkeypatch.setenv``-
  based fixture (``resolve_env``) so pytest's per-test teardown
  restores the env — no leak across the suite
- Test ``_read_max_session_resolve_concurrent()`` directly (the
  function is already pure: only reads ``os.getenv``) so no module
  reload is needed at all
- The single test that needs the module-level constant asserts
  it equals the helper's return value at import time, which is
  the real production contract

Better still (maintainer's hint): drive the test through the
pure helper instead of the module-level constant. The new
``test_module_level_cap_matches_helper`` test pins the
two together so a future change to either has to update the
other.

**Tests** (14 total, all green; 102/102 with the cross-suite
pollution check):

- 3 default (unset / empty / whitespace)
- 2 in-range override (mid / upper bound 64)
- 5 bad input (0 / negative / above 64 / non-numeric / empty-after-strip)
  — all fall back to 2, never to the permissive extreme
- 1 module-level cap reflects helper (the new equivalence pin)
- 1 semaphore capacity matches cap (the safety-lock reflection)
- 1 source-shape guard: helper is defined and the cap constant
  is initialised from it
- 3 profile-layer guards: protected env keys, blocked runtime
  env keys, filter strips the cap (the round-2 / round-3 fix
  for #7655-style profile .env resizing)
- 1 cross-suite pollution guard (AST-aware: forbids
  ``sys.modules.pop``, ``importlib.import_module``,
  ``os.environ[``, ``os.environ.pop(``)

**Verification**:
- ``pytest tests/test_issue7421_resolve_cap_config.py`` → 14 passed
- ``pytest tests/test_issue7421_resolve_cap_config.py tests/test_session_sidecar_repair.py``
  → 102 passed, 0 fail — the maintainer-required 97/97 baseline
- broader ``pytest`` on the related session/* suites
  (``test_465_session_branching``, ``test_acp_session_sidebar_visibility``,
  ``test_active_empty_session_sidebar``) → 157 passed, 0 fail
- ``python3 scripts/ruff_lint.py --diff origin/master`` → "no new
  violations on added/modified lines. OK."
2026-09-23 17:32:35 +08:00
happy5318 d59738f2f1 fix(api): typo-safe resolve-cap fallback + profile .env cannot resize (#7656 round 3)
Two findings from the round-1 maintainer review were not addressed
in round-2: the helper clamped an out-of-range value *up* to the
ceiling, and the env var was exposed to per-profile ``.env``
overrides, which contradicts the "process-wide resource limit"
intent and creates a first-load-wins asymmetry across profile
switches. This round fixes both.

``_read_max_session_resolve_concurrent()`` previously treated
out-of-range values like ``HERMES_WEBUI_MAX_SESSION_RESOLVE=999999``
as "the operator wants the maximum" and returned ``64`` — admitting
64 simultaneous unbounded transcript loads. The round-1 reviewer
measured ~9.6 MB per parse on a synthetic 4.27 MB transcript, so the
old ceiling permitted ~614 MB at 64-way concurrency before any
other application memory.

The cap is a *safety bound*, not a user preference. A typo must
fall back to the safe default, never to the permissive extreme —
matching the behavior of every other bad input in this helper
(missing/empty/zero/negative/non-numeric all already fall back to
2). The 64 ceiling is reachable only via an explicit in-range
operator value.

server-wide key, not a per-profile .env override (SILENT)

The cap controls a single process-wide ``BoundedSemaphore`` that
gates full-transcript resolves. A profile's ``.env`` is per-profile
by design; letting it set a server-wide limit lets a single
profile drag the rest into a tighter or looser posture than the
operator intended, with a first-load-wins asymmetry (a later
profile switch cannot resize the already-constructed semaphore).

The same shape as the isolated-profile key added in #4589: only
the operator/launcher env at startup can set it. The cap is added
to both ``_PROTECTED_ENV_KEYS`` (blocks the profile env at the
load step) and ``_BLOCKED_RUNTIME_ENV_KEYS`` (filters the profile
env in the runtime/gateway-parity path), so a profile cannot
inject the env var at any layer.

1. ``api/models.py`` — ``_read_max_session_resolve_concurrent()``:
   - ``if n > 64: return 64`` → ``if n > 64: return 2``
   - Docstring updated to spell out the "safety bound, not a user
     preference" rule and to point at ``_PROTECTED_ENV_KEYS`` as
     the place the 64 ceiling is enforced for profile env.

2. ``api/profiles.py`` — two frozensets:
   - ``_PROTECTED_ENV_KEYS`` adds
     ``'HERMES_WEBUI_MAX_SESSION_RESOLVE'`` (next to
     ``'HERMES_WEBUI_ISOLATED_PROFILE'``).
   - ``_BLOCKED_RUNTIME_ENV_KEYS`` adds the same key, with a
     comment explaining the first-load-wins asymmetry.

3. ``tests/test_issue7421_resolve_cap_config.py``:
   - The round-1 ``test_env_above_upper_bound_is_clamped_to_64``
     is renamed to ``test_env_above_upper_bound_falls_back_to_default``
     and the assertion + docstring reflect the new fallback
     contract. The maintainer's review said explicitly:
     "Update the clamp test accordingly."
   - Three new tests pin Finding 2:
     - ``test_protected_env_keys_includes_resolve_cap``
     - ``test_blocked_runtime_env_keys_includes_resolve_cap``
     - ``test_filter_runtime_env_strips_resolve_cap`` (end-to-end
       exercise: a profile env containing the cap is filtered out
       of the gateway-parity view, and an unrelated, non-blocked
       key like ``LANG`` still passes through as a sanity check)

- ``tests/test_issue7421_resolve_cap_config.py``: 15/15 pass.
- ``-k "profile or protect or isolated or HERMES_WEBUI_ISOLATED"``:
  866 passed, 5 skipped, 1 xfailed, 2 xpassed — no regression.
- The 5 ruff findings on the worktree are all pre-existing
  (3× F401 ``sqlite3`` + 2× B904 ``raise-with-from`` in
  ``api/profiles.py``); the curated gate is gated on
  newly-introduced findings only.

Two. Both are the explicit fixes the maintainer requested.

- ``_read_max_session_resolve_concurrent(999999)`` now returns
  ``2`` instead of ``64``. The 64 ceiling is still reachable
  via an explicit in-range operator value.
- ``HERMES_WEBUI_MAX_SESSION_RESOLVE`` set in a profile's
  ``.env`` is now ignored: the operator/launcher env at
  startup is the only place the cap is configurable.

Both changes are backward compatible at the operator level
(an explicit ``=8`` still returns ``8``) and break the
"typo → ceiling" failure mode and the
"first-loaded-profile-wins" asymmetry the maintainer flagged.

- An operator who set ``HERMES_WEBUI_MAX_SESSION_RESOLVE=64``
  in their deployment env is unaffected (still returns 64,
  as before).
- An operator who set ``HERMES_WEBUI_MAX_SESSION_RESOLVE=128``
  in their deployment env now silently falls back to 2 instead
  of getting 64. This is the round-1 reviewer's intent:
  "Update the clamp test accordingly" + "the safe default,
  never the permissive extreme." A startup log warning could
  be added in a follow-up if a louder signal is wanted; this
  PR keeps the silent-fallback contract consistent with the
  other bad-input paths in the same helper.
- A profile that *had* ``HERMES_WEBUI_MAX_SESSION_RESOLVE=64``
  in its ``.env`` and was relying on it to set the cap now
  silently uses the operator-set value. This is the intended
  behavior: the cap is a process-wide resource limit, not a
  per-profile toggle. A profile-side log warning is a
  follow-up if needed; the runtime env path now drops the
  key, so the failure mode is "operator value wins" rather
  than "first profile to load wins".

- #7656 — the original PR; this is round-3 of the same branch.
- #7421 — the original env-override feature.
- #4589 — the same shape ("operator-only, not per-profile")
  for the isolated-profile key; the comment block on the new
  protected key references the same pattern.
2026-09-23 17:32:35 +08:00
happy5318 31e22a152b feat(api): make _FULL_SESSION_RESOLVE_MAX_CONCURRENT configurable via env (#7421)
[rebase onto current master — same content as 5a7f7d8, no functional
changes beyond a single context-line merge from master]

Squash-rebase of #7656 onto current master so the 3-way merge CI
test no longer pulls in the unmerged stage/7618-7619-renderer-i18n
dirty diff that 5a7f7d8 was based on. The actual change set is
unchanged: 2 files, +231/-1.

What changed (from #7656 / 5a7f7d8):

- api/models.py::_read_max_session_resolve_concurrent — new
  module-level helper that reads the cap from the env var
  HERMES_WEBUI_MAX_SESSION_RESOLVE with strict validation
  (positive int in [1, 64], malformed → default 2).
- tests/test_issue7421_resolve_cap_config.py — 12 regression
  tests covering unset/empty/whitespace, headline cases (4/8/64),
  safety paths (0/-1/999999/non-numeric), whitespace tolerance,
  BoundedSemaphore capacity check, and a static wiring assertion.

Model: MiniMax-M3
2026-09-23 17:31:33 +08:00
nesquena-hermes 5e16dd860f Merge pull request #7764 from nesquena/stage/7708-media-backtick
Release: #7708 stop MEDIA: tokens swallowing a closing backtick (@happy5318)
2026-09-23 02:27:13 -07:00
happy5318 70646b3502 fix(i18n): explicit server tri-state for language + reviewer 4-row table regression (#7730 round 4)
Round-3 removed the `"language": "en"` schema default so fresh installs stopped
reporting a fake explicit English, and dropped the `primary === 'en'` skip.
The maintainer's round-4 gate asked to complete the tri-state shape:

1. `load_settings()` now emits an explicit `None` for the unset case
   (`settings.setdefault("language", None)`) so `/api/settings` carries a
   real three-way signal instead of omitting the key: `null` = no
   preference, "en" = explicitly saved English, any other code = chosen
   locale.  Stored values (including legacy pre-change English picks) still
   win via the merge and are untouched.
2. New `TestReviewerTableServerTriState` regression loads the real
   `static/i18n.js` in a Node `vm` sandbox and drives
   `resolvePreferredLocale(s.language, localStorage.getItem('hermes-lang'),
   _detectBrowserLanguageHint())` — the exact boot.js call shape — for all
   four rows of the maintainer's reproduction table:
   - `en` + new browser (zh-CN) -> en   (was zh under round-2 skip)
   - `en` + stale ja + en-US -> en      (was ja under round-2 skip)
   - `en` + empty + en-US -> en
   - `fr` + empty + zh-CN -> fr

Verification:
- `python3 -c "import api.config"` OK
- 15/15 in test_issue7622_initial_locale_from_browser.py (14 + new table test)
- 8/8 in test_language_precedence.py
- 60/60 settings + 13/13 password-env + login-locale/autosave/sprint26
  collateral tests pass
- ruff_lint --diff origin/master: 0 new violations
2026-09-23 17:03:27 +08:00
happy5318 ff8e75c0be docs(models): document the /api/models/live allowlist runtime contract
Addresses the standing documentation finding: records the four evaluation
signals (discovered catalog defers, plural allowlist filters, singular model
never gates, no allowlist shows full catalog), serialized-list shapes, the
empty-allowlist-is-not-configured rule, and the probe-failure fallback.
Registered in docs/CONTRACTS.md per the #7556 precedent.
2026-09-23 16:58:09 +08:00
happy5318 18842cc707 fix(models): discovered catalogs are discovery metadata, not allowlists
Hermes persists discovery results as models: {...} + models_discovered: true.
_custom_provider_allowlist_ids() treated any plural models value as an
explicit allowlist, so after one discovery /api/models/live was permanently
pinned to the first-discovery set and newly available upstream models were
silently dropped (gate repro: new-live-c missing vs merge-base).

Gate the helper on api.config._provider_models_are_discovered_catalog() —
the same predicate the /api/models path uses — and return no allowlist when
it holds. discover_models: false still pins via the predicate; a hand-written
models: list without the discovered marker still filters; probe-failure
fallback still serves saved ids.

Real-handler tests: discovered catalog preserves full live catalog (RED on
the previous head), discover_models: false control stays pinned, failed-probe
control still returns saved models.
2026-09-23 16:58:09 +08:00
happy5318 39017f6013 chore: retrigger CI matrix (shard 0 flake, green locally on 3 bases) 2026-09-23 16:58:09 +08:00
happy5318 4b689ecbce fix(models): enforce plural allowlist contract on failed live probes 2026-09-23 16:58:09 +08:00
happy5318 88236dbc26 docs(models): pin empty-allowlist semantics with regression coverage
Greptile flagged that `models: []` / `models: "[]"` falls through the
non-empty allowlist gate and leaves the full live catalog selectable. The
symptom is real, but implementing the ask (gate on declared-ness instead)
measurably makes things worse: it emits an EMPTY picker and leaves the
provider unselectable.

Measured both branches against the real handler before deciding:

  models key absent   declared=False -> [chat-a, chat-b, image-gen-1, audio-tts-1]
  models: []          declared=True  -> []   EMPTY PICKER
  models: "[]"        declared=True  -> []   EMPTY PICKER
  models: [chat-a]    declared=True  -> [chat-a]

An unselectable provider is a harder failure than a few extra visible
models, and it is unrecoverable from the UI: `custom_providers` has no
WebUI write path, so it is hand-edited in config.yaml. There is also no
deny-all use case — a provider the user wants hidden is removed from
`custom_providers` outright, not left behind with an empty allowlist.

No production behavior change. Records the trade-off in the filter block
and adds `test_empty_allowlist_is_treated_as_not_configured` (native empty
list, serialized "[]", empty mapping, blank scalar) so a future "fix" of
this edge case fails loudly instead of silently emptying the picker.

Verified non-vacuous: applying the declared-ness gate fails all 4 new
cases with `assert [] == [...]`.
2026-09-23 16:58:09 +08:00
happy5318 4a1fb47ce5 fix(models): respect config model allowlist in live custom-provider catalog
Custom providers (esp. New-API-style gateways) expose their ENTIRE catalog
via /v1/models, including image/audio models that aren't chat models. The
live-catalog enrichment surfaced all of them in the model picker regardless
of the `models` allowlist declared in config.yaml.

Filter the live catalog down to the explicit plural `models` allowlist, then
append any allowlisted models the upstream didn't return. Only the plural
`models` key gates the filter — a provider configured with just a singular
`model` (sticky/default metadata) keeps the unfiltered live catalog so
discovery still works.

Decode the plural value through `_parse_config_string_list()` so serialized
shapes are honored: `hermes config set` and JSON-mode editor saves persist
lists as quoted JSON-array strings ('["chat-a","chat-b"]') or Python
literals ("['chat-a']"). A dict/list-only parser dropped those to [], which
the caller reads as "no allowlist configured" → full catalog floods the
picker (same class as the skills.disabled regression, #7120/#7134). Native
dict/list values are still walked directly so dict entries keep their
id|model|name metadata, which the decoder would str()-ify.

Tests invoke the real `_handle_live_models` and assert on the emitted
`models` payload, covering: native list, JSON-array string, Python-literal
string, mapping metadata, list-of-dicts, singular-model-only (not gated),
malformed serialized input (safe), allowlisted model missing from live
(appended), and probe failure (config fallback).

Verified non-vacuous: removing the production filter block fails 7 of 9
cases; reverting only the string decode fails exactly the 3 serialized
cases with the documented flood symptom (image/audio models reappear).
2026-09-23 16:58:09 +08:00
nesquena-hermes 86d5b36898 Merge master (exp-v0.52.349) into stage/7708-media-backtick
# Conflicts:
#	CHANGELOG.md
2026-09-23 08:50:25 +00:00
nesquena-hermes 19b00f5e37 Merge pull request #7762 from nesquena/stage/7749-terminal-handoff
Release: #7749 preserve the chat terminal handoff when the sidebar reports idle first (@franksong2702)
2026-09-23 01:48:58 -07:00
nesquena-hermes 69655b1fc6 Merge master (exp-v0.52.348) into stage/7749-terminal-handoff
# Conflicts:
#	CHANGELOG.md
2026-09-23 08:37:23 +00:00
nesquena-hermes 6a41c23cf7 Merge pull request #7757 from nesquena/stage/7751-journal-seq
Release: #7751 allocate run-journal sequence inside the append transaction (@franksong2702)
2026-09-23 01:35:39 -07:00
Anthony Ruiz 9b6851eaf1 fix(background_process): do not consume the delivery budget on transient wake-up refusals
start_session_turn() answers 409 retryable=True while the Agent runtime is
stale, 409 process_wakeup_paused while wake-ups are paused, and 409 with
active_stream_id while the session is busy with another turn. The bridge
released the durable claim as a plain failure each time, so a burst of
refusals within seconds terminally dropped a completion whose origin session
was alive and waiting.

release_async_delegation_delivery(..., retryable=True) now forwards the
refund to an Agent core whose release_event_delivery() accepts the
`retryable` keyword (NousResearch/hermes-agent#120028), detected through
inspect.signature, and falls back to the historical plain release on an
older core. Hard rejections keep the bounded budget.
2026-09-23 08:32:08 +00:00
Anthony Ruiz 936710dc1d fix(streaming): surface Agent session-lease waits as warnings
When another Hermes process holds a session's turn lease, the Agent
(agent/turn_facade_lease.py) emits lifecycle statuses while waiting and on
admission, and a warn status when the wait times out and the message was
not processed. _agent_status_callback dropped all of them, so a WebUI turn
looked stuck with no explanation.

Classify these notices by Agent status kind (lifecycle/warn) plus the
Agent's exact phrases, and relay them as {'type': 'session_lease_wait'}
warning events rendered by the existing messages.js warning listener.
2026-09-23 08:31:39 +00:00
nesquena-hermes d06ee674a3 Merge master (exp-v0.52.347) into stage/7708-media-backtick
# Conflicts:
#	CHANGELOG.md
2026-09-23 08:29:47 +00:00
nesquena-hermes adda410ec0 Merge master (exp-v0.52.347) into stage/7749-terminal-handoff
# Conflicts:
#	CHANGELOG.md
2026-09-23 08:29:47 +00:00
nesquena-hermes 64106a790d Merge master (exp-v0.52.347) into stage/7751-journal-seq
# Conflicts:
#	CHANGELOG.md
#	docs/rfcs/webui-run-state-consistency-contract.md
2026-09-23 08:28:18 +00:00
nesquena-hermes 642230b355 Merge pull request #7756 from nesquena/stage/7748-publication-fence
Release: #7748 fence Agent publication/cleanup with Stop ownership (@franksong2702)
2026-09-23 01:26:49 -07:00
nesquena-hermes 398d9495ef docs(changelog): #7749 2026-09-23 06:25:04 +00:00