Commit graph

8481 commits

Author SHA1 Message Date
Teknium
4c64ff3aa0
feat(nous): send top-level session_id for provider sticky routing (#69253)
* feat(nous): send top-level session_id for provider sticky routing

The Nous Portal profile only embedded the session id inside portal tags,
so Claude traffic through the portal had no sticky-routing key. Multi-turn
sessions could reroute between upstream endpoints (Anthropic/Vertex/
Bedrock), cold-writing a fresh prompt cache on every reroute since each
provider's cache is instance-local.

Mirror the OpenRouter profile: emit extra_body.session_id whenever the
agent has one, pinning every turn of a session to the same endpoint so
explicit cache_control breakpoints stay warm.

* test: expect top-level session_id in Nous max-iterations summary body

Sibling site of the profile change — the max-iterations summary path
builds its request through the same NousProfile.build_extra_body(), so
its exact-shape assertion now includes the sticky-routing session_id
when the agent has a session.
2026-07-22 04:39:20 -07:00
Teknium
0c4cab56b4 test(secrets): match real ApplyReport shape in isolation test
The fake apply_all in test_external_secret_values_are_isolated_between_homes
returned an ApplyReport with no SourceReports; since #69056 the env_loader
marks _APPLIED_HOMES (and records snapshots) only when at least one enabled
source actually reported, so the fake must include a SourceReport like the
real orchestrator always does.
2026-07-22 04:39:17 -07:00
izumi0uu
0583692c2d fix(secrets): scope BWS-injected provider keys
Snapshot values applied by external secret sources per resolved HERMES_HOME so a later profile cannot replace an earlier profile scope through shared os.environ.

Keep provider and credential-pool fallback reads on the active secret scope, and fail closed on unscoped multiplex reads.

Tests: scripts/run_tests.sh tests/test_env_loader_secret_sources.py tests/test_env_loader_op_bootstrap.py tests/agent/test_secret_scope.py tests/agent/test_credential_pool.py tests/tools/test_credential_pool_env_fallback.py tests/hermes_cli/test_xiaomi_provider.py tests/cron/test_run_one_job.py tests/hermes_cli/test_api_key_providers.py tests/gateway/test_multiplex_credential_isolation.py -q (395 passed)
2026-07-22 04:39:17 -07:00
yungchentang
c6f9e0c748 test(gateway): cover routed transport delivery 2026-07-22 04:19:10 -07:00
yungchentang
ff46376614 fix(gateway): preserve shared route transport adapter 2026-07-22 04:19:10 -07:00
Teknium
86fb046383
feat(secrets): orchestrator-level preserve_existing + profile aliasing (#69058)
Fixes the profile-clobber bug cluster at the apply_all() chokepoint so
every secret source — bundled and plugin — gets both behaviors for free:

- secrets.preserve_existing (#58073): env var names whose existing .env /
  shell value always wins, even against a source with
  override_existing: true.  Escape hatch for per-profile platform
  secrets while everything else rotates centrally.
- Profile aliasing (#51447): under a named profile, an applied
  FOO_<PROFILE> var (credential-shaped suffixes only) also hydrates the
  canonical FOO, so adapters/plugins that read fixed env names see the
  profile's value.  Direct supply beats alias; protected/claimed/
  override guards all apply; secrets.profile_alias: false disables.

Reimplements the intent of PR #58085 (tianma-if, preserve_existing on the
legacy Bitwarden apply shim) and PR #51616 (LeonSGP43, profile aliasing
inside the Bitwarden backend) on the SecretSource orchestrator that
superseded those code paths.

Fixes #58073.  Fixes #51447.

Co-authored-by: tianma-if <5895871+tianma-if@users.noreply.github.com>
Co-authored-by: LeonSGP43 <154585401+LeonSGP43@users.noreply.github.com>
2026-07-22 03:20:45 -07:00
Teknium
e66e02dc83 test(gateway): activate multiplex flag in cross-profile env isolation test
The test installs a secret scope and asserts a scope miss does NOT fall
back to the default profile's env — that isolation guarantee only holds
under multiplexing, which the real gateway activates at startup via
set_multiplex_active().  With the #67827 overlay fallthrough (scope miss
→ os.environ when multiplex is OFF), the test needs to model the
multiplexed runtime it is actually testing.
2026-07-22 03:20:35 -07:00
Soju06
c758ded6d2 fix(secrets): fall back to os.environ on scope miss when multiplexing is off
fdab380a1 wraps every cron job in a <home>/.env secret scope regardless of
deployment mode. get_secret() treats any installed scope as authoritative,
so in single-profile deployments where provider keys live only in the
process environment (systemd Environment=, pass-cli/op run wrappers, shell
exports) every cron credential read returns empty, the OpenAI client is
built with the no-key-required placeholder, and each scheduled job 401s —
while interactive turns keep working. Scope-miss reads now fall through to
os.environ when multiplexing is off; multiplexed scopes stay authoritative.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 03:20:35 -07:00
Teknium
c7b0c0d35f
fix(secrets): mark _APPLIED_HOMES only after a real fetch attempt (#40597) (#69056)
_apply_external_secret_sources() added the home to _APPLIED_HOMES before
loading config, so a malformed config.yaml, a missing secrets section, or
all-sources-disabled permanently disabled secret loading for the process
— even after the user fixed the config.  Long-lived processes (gateway)
never recovered without a restart.

Now the home is marked only after apply_all() actually ran with at least
one enabled source.  Fetch errors still mark the home (so import-time
load_hermes_dotenv() calls don't re-fetch and re-print the same failure
3-5x per startup); the cheap early-exit paths stay retryable.

Fixes #40597.
2026-07-22 03:20:25 -07:00
izumi0uu
8e089db689 fix(secrets): validate bitwarden status token
Keep the env-presence row, but add a real Bitwarden probe so revoked or malformed tokens no longer look healthy in hermes secrets bitwarden status.

Also document the new status behavior and lock it in with a dedicated regression test.

Refs: NousResearch/hermes-agent#40275
Tested: ./scripts/run_tests.sh tests/hermes_cli/test_bitwarden_status.py tests/test_bitwarden_secrets.py
Tested: .venv/bin/python -m ruff check hermes_cli/secrets_cli.py tests/hermes_cli/test_bitwarden_status.py
2026-07-22 03:20:09 -07:00
andrexibiza
fe5d0be6db fix(env): stop printing Bitwarden secret names 2026-07-22 03:19:58 -07:00
SeoYeonKim
2beed8b53d fix(mcp): pass secret-source-injected env vars to stdio servers
Surgical reapply of PR #37523 onto current main (the original branch
predates the SecretSource registry refactor).  _build_safe_env() now
forwards env vars tagged in env_loader._SECRET_SOURCES — widened from
Bitwarden-only to any registered secret source (Bitwarden, 1Password,
plugin backends), since the provenance map is source-agnostic.
Explicit server env: config still wins; untagged secrets stay filtered.

Fixes #37499.
2026-07-22 03:19:42 -07:00
menhguin
fe8c0f7eef fix(secrets): pass OP_LOAD_DESKTOP_APP_SETTINGS through to the op child env
The 1Password secret source builds a minimal allowlisted environment for the
`op read` child process. The allowlist omits OP_LOAD_DESKTOP_APP_SETTINGS, so a
user who exports it (shell, .env, or service unit) sees it silently stripped
before it reaches `op`.

That var is `op`'s documented switch to skip the desktop-app integration probe.
When the 1Password desktop app is installed, `op` probes its settings/socket at
startup *before* evaluating service-account auth. If the desktop app's group
container is wedged (e.g. macOS 'Interrupted system call' on the 1Password group
container), that probe blocks with no timeout, so `op read` hangs indefinitely
even with a valid OP_SERVICE_ACCOUNT_TOKEN present. Setting
OP_LOAD_DESKTOP_APP_SETTINGS=false is the intended escape hatch — but stripping
it means it has no effect on exactly the headless boxes that need it.

Fix: add OP_LOAD_DESKTOP_APP_SETTINGS to _OP_ENV_ALLOWLIST so the documented
var reaches the child. No behavior change when it's unset. Adds a focused test
alongside the existing allowlist test.

Repro: on a machine with a wedged 1Password desktop container + a valid SA
token, `op read` hangs 600s+ without the var and returns in ~4s with it — but
only if it actually reaches the op process, which this allowlist entry ensures.

Co-authored-by: Minh Nguyen <menhguin@users.noreply.github.com>
2026-07-22 03:19:32 -07:00
briandevans
a616b9fb0f fix(secrets): fold OP_CONNECT_HOST/OP_CONNECT_TOKEN into 1Password auth cache-key
_auth_fingerprint() built the 1Password secret cache-key from the
service-account token, OP_ACCOUNT, and OP_SESSION_* vars but omitted
OP_CONNECT_HOST/OP_CONNECT_TOKEN, which are in _OP_ENV_ALLOWLIST and are
forwarded to the op child (the Connect-server auth path). Rotating
OP_CONNECT_TOKEN or re-pointing OP_CONNECT_HOST at a different Connect
identity left the fingerprint unchanged, so both the in-process and disk
caches kept serving secrets resolved under the old Connect credentials for
the full TTL (default 300s, disk-persisted across invocations). This
contradicts the function's own docstring invariant that a value cached
under a previous identity is never served under a new one; it closes the
gap for the Connect path, matching the OP_SESSION_*/service-account paths
that are already protected.
2026-07-22 03:19:32 -07:00
JackJin
7db521a697 fix(secrets): port stale-cache fallback to current DiskCache API + gate by error kind
The stale-fallback branch called _read_disk_cache(), a helper removed in
db495b0fba when disk-cache logic moved to the
shared DiskCache class — every fallback attempt raised NameError instead of
serving cached secrets, silently defeating the PR's whole purpose. Port to
_DISK_CACHE.read().

Also tighten the fallback per DiskCache's TTL contract and the secret-source
error taxonomy:
- Gate on cache_ttl_seconds > 0 so a caller that opted out of caching
  entirely (ttl=0) never gets a secret value that didn't come from a live
  fetch, even on the failure path.
- Gate on _classify_bws_error(str(exc)) being NETWORK or TIMEOUT, reusing
  the existing classifier — an AUTH_FAILED or malformed-output failure must
  still raise, since serving stale secrets there would mask a real
  credential/config problem instead of a transient outage.

Ported the test helpers off the removed _write_disk_cache to a direct JSON
write (matching this file's existing disk-cache test convention) and added
tests for the auth-failure, malformed-output, and zero-TTL gates. Reverting
the fix and re-running confirms 7 of 8 stale-fallback tests fail with the
original NameError.
2026-07-22 03:19:16 -07:00
JackJin
77f3b3ef7f fix(secrets): fall back to stale disk cache when bws live fetch fails
Without this, a single DNS hiccup or BWS outage at gateway startup leaves
the whole fleet running with an empty credential pool — every model call
fails until someone restarts after the network recovers.  When a previous
successful fetch already populated the disk cache, return those secrets
with an explicit warning instead of raising RuntimeError.

`use_cache=False` (explicit opt-out) still raises so manual flows like
the setup wizard surface the original error.  The disk cache is not
re-written on the fallback path so a process restart still triggers a
proper TTL re-check.

Fixes #41925
2026-07-22 03:19:16 -07:00
Hao Zhe
8af2133009 fix(openviking): align session context with shared profile contract 2026-07-22 14:12:50 +05:30
Flownium
11c1ca01c5 fix(openviking): inject session-start memory context
(cherry picked from commit 18b474d0bd)
2026-07-22 14:12:50 +05:30
kshitij
c3c80e1796 refactor: cleanup follow-up for salvaged PR #58871
- Remove dead current_sid parameter from _recover_pending_sessions
- Remove dead cleanup parameter from _release_owner_run_claim (always True)
- Set _run_lock_path after flock succeeds, not before
- Collapse redundant BlockingIOError branch (covered by OSError+errno check)
- Track _pending_marked_sids to skip re-writing marker file on every sync_turn
2026-07-22 14:05:28 +05:30
Chris Korhonen
81fc424592 fix(openviking): chunk structured session sync
Preserve ordered structured turns across OpenViking's 100-message batch limit and resume retries from the first unconfirmed message.

Based on the OpenViking batching work from commit 1a567f7067 in #58981.
2026-07-22 14:05:28 +05:30
Hao Zhe
3fd96583f4 fix(openviking): serialize orphan session recovery 2026-07-22 14:05:28 +05:30
Hao Zhe
323e9baf5d fix(openviking): recover pending session commits 2026-07-22 14:05:28 +05:30
kshitijk4poor
9ecacd6bf4 fix(telegram): update auth check tests for group_allow_from split
Update test_telegram_auth_check.py to use group_allow_from for group
messages (matching the PR's intentional behavior split: allow_from for
DMs, group_allow_from for groups). Add test_is_user_authorized_from_message_group_allow_from
to cover the new group path.
2026-07-22 12:47:41 +05:30
kshitijk4poor
7078430934 fix(telegram): address review findings from PR #67816
- Update test_observed_group_context_preserves_slash_command_text_for_dispatch
  to assert user_id is preserved for COMMAND messages (new correct behavior)
- Add _coerce_allow_set helper to handle both list and comma-separated
  string allowlist inputs (prevents character-by-character iteration bug)
- Include 'channel' in chat_type checks for group-scoped authorization
- Add _telegram_extra fallback for group_allowed_chats (consistent with
  group_allow_from fallback)
- Add AUTHOR_MAP entry for nyaruko@hermes -> tsuk1nose
2026-07-22 12:47:41 +05:30
McHermes
f4896015c1 fix(compression): ignore assistant handoff summaries in tail anchor
Assistant-role compaction summaries were treated as the last visible assistant reply after head protection decayed. That pulled the tail boundary back to the summary itself and left zero new turns to summarize.

Exclude internal context summaries from both the visible-reply search and the assistant fallback, mirroring the existing user-role summary exclusion.
2026-07-22 11:46:05 +05:30
kshitijk4poor
9fa2906c18 fix: restore base_url rstrip, extract should_clear_context_pin helper
Salvage follow-up for PR #68899:
- Restore .rstrip('/') on base_url in _swap_credential (both anthropic
  and OpenAI paths) to match every other assignment site. The route
  identity comparison still uses normalize_route_base_url which handles
  trailing slash correctly.
- Extract should_clear_context_pin() into hermes_cli/route_identity.py,
  consolidating 7 copy-pasted call sites across cli.py, gateway/run.py,
  gateway/slash_commands.py, and hermes_cli/model_switch.py into a
  single fail-closed helper.

C1 (anthropic path TLS re-application): pre-existing gap — the Anthropic
adapter (build_anthropic_client) has no TLS customization support at
all, so this is out of scope for this salvage.
2026-07-22 11:19:37 +05:30
cucurigoo
63dd651b3d fix(providers): scope route-owned runtime settings 2026-07-22 11:19:37 +05:30
cucurigoo
f3f0135154 fix(providers): fail closed on missing active route 2026-07-22 11:19:37 +05:30
cucurigoo
ddd667503e test(providers): cover URL whitespace route identity 2026-07-22 11:19:37 +05:30
cucurigoo
639cee5216 test(providers): complete hermetic route coverage 2026-07-22 11:19:37 +05:30
cucurigoo
2507af2194 test(providers): cover query path slash identity 2026-07-22 11:19:37 +05:30
cucurigoo
fcae6fb9b8 test(providers): cover route URL identity boundaries 2026-07-22 11:19:37 +05:30
cucurigoo
ca6b8cd85f test(compression): cover overflow after blocked preflight 2026-07-22 11:19:37 +05:30
cucurigoo
cb785e6b49 fix(providers): align custom route scoping 2026-07-22 11:19:37 +05:30
cucurigoo
97499d702e fix(compression): harden startup route scoping 2026-07-22 11:19:37 +05:30
cucurigoo
377244f7c8 fix(compression): prevent stale-budget retry loops 2026-07-22 11:19:37 +05:30
HexLab98
b7b0c37ef7 test(update): cover fleet restart timeout isolation (#68523) 2026-07-22 10:35:50 +05:30
anoopmehendale-cue
2ab153218b fix(gateway): make adapter fatal-error handoff cancellation-proof; exit if a platform is stranded
The fatal-error notification runs on the failing adapter's own polling
task, and adapter.disconnect() inside the handler can cancel that task
(its current-task guard misses because _safe_adapter_disconnect runs the
close in a wrapper task). The CancelledError killed the handler between
the fatal log and the reconnect queue, leaving the platform permanently
dead inside a live gateway process. #68447 fixed this for telegram at
the adapter layer; this hardens the shared gateway dispatch so every
platform gets the same protection (qqbot #25505/#29005, photon #68693).

- _handle_adapter_fatal_error now runs the real handler in a detached
  task, awaited through asyncio.shield() so caller cancellation cannot
  tunnel into it (Task.cancel() also cancels the task's _fut_waiter).
- If a retryable platform still ends up neither reconnected nor queued,
  the gateway exits with failure so launchd/systemd KeepAlive restarts
  it instead of running indefinitely with a dead platform (#68693).

Fixes #68693

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 10:31:59 +05:30
kshitijk4poor
e57918ac80 test: update mock assertions for conversation_history kwarg
The /branch and /resume flush tests asserted the old positional-only
call signature. Update to match the fix from #68480.
2026-07-22 10:00:39 +05:30
kshitijk4poor
a46fbafe31 test: drop source-grep change-detector from #68480
The three behavior tests (control proves dup, boundary is noop, tail-only
write) fully cover the flush semantics. The source-grep test reading
cli.py + cli_commands_mixin.py as text and asserting a string appears is
a change-detector that breaks on benign refactors without adding coverage.
2026-07-22 10:00:39 +05:30
Bartok9
1c0a57832b fix(cli): pass conversation_history on /new /resume /branch flush
Closes #68454

Root cause: cold-resumed transcript rows lack _DB_PERSISTED_MARKER until a
normal turn flush stamps them. Immediate /new,/resume,/branch flushed with
no history boundary, so every restored row was re-appended to the old session.

Fix: pass conversation_history=self.conversation_history at all three sites
(mirrors #68205). Add offline regression coverage for noop + tail-only write.

Verification: pytest tests/agent/test_session_rotation_flush_cold_resume_68454.py (4 passed)
2026-07-22 10:00:39 +05:30
kshitijk4poor
4425ddd94d fix(gateway): hard-exit on KeyboardInterrupt path too
The KeyboardInterrupt handler in run_gateway() was the only exit path
that still used bare 'return' instead of _hard_exit_after_gateway_teardown().
While less common than service-managed restarts, a console Ctrl+C still
leaves the process vulnerable to the same Python finalization hang on
non-daemon worker threads (cron ThreadPoolExecutor jobs). Route it through
the same backstop, with a 'return' guard for test stubs that don't raise
on code 0 (production os._exit never returns).
2026-07-22 09:53:51 +05:30
web3blind
c9ab8baf31 test: update gateway run stub for hard-exit helper 2026-07-22 09:53:51 +05:30
web3blind
fd96e138b6 fix(gateway): hard-exit CLI runner after graceful teardown 2026-07-22 09:53:51 +05:30
Siddharth Balyan
9baad4e0aa
feat(cli): plan catalog on Free + plan= deep link + top-up/auto-refill copy split (#68689)
* feat(cli): plan catalog on Free + plan= deep link + top-up/auto-refill copy split

Bring the plain (non-TUI) CLI billing surface to parity with the desktop/TUI
billing changes:

- /subscription on Free (admin/owner, interactive) prints the plan catalog
  (name · $/mo · $credits/mo, from the same tiers[] data the TUI uses; monthly
  credits render as dollars). A numbered pick opens the manage-subscription
  deep-link directly with plan=<tier_id> appended.
- subscription_manage_url(state, tier_id=...) appends plan=<tier_id> (the stable
  tiers[] id) when a tier was picked, org_id first — mirrors the TUI's ?plan=.
  The paid change flow's blocked/unknown-preview portal fallback carries plan=
  for upgrades only; downgrades stay generic/native.
- /topup overview splits one-time top-up from automatic refill, the distinction
  stated in each first sentence ("Add funds now — a single charge…" vs "Refill
  when low — charges … automatically …"), keeping "credits" out of the
  dollars-only surface.
- Downgrades remain native (chargeless scheduled change), unchanged.

Updates the CLI-parity section of docs/billing-lifecycle.md and tests under
tests/hermes_cli + tests/agent.

* refactor(billing): share plan-catalog helpers + harden manage-url builder

- subscription_manage_url now preserves unrelated portal query params (parse_qsl,
  popping only the contract-owned org_id/plan) and restricts to http/https schemes,
  matching the desktop URL builder — the function owns the contract.
- Lift the plan-catalog derivation into agent/subscription_view.py so the CLI Free
  catalog and the paid picker/blocked-preview branch share one implementation:
  selectable_tiers (enabled paid, not current, sorted), format_tier_row (name · $/mo
  · $credits/mo — thousands-grouped like the TUI's toLocaleString, credits suffix
  hidden when absent/zero), and is_upgrade(state, tier_id).

* fix(cli): numbered pick, canonical guarded browser opener, partial auto-refill copy

- Free catalog: accept a bare digit as a pick (the shared normalizer only knows the
  confirm-dialog digit aliases, so `1` used to resolve to None → "Cancelled"). The
  Nth digit maps to the Nth printed row.
- Extract one _open_url_in_browser used by every "open the portal" path, applying the
  device-code flows' console-browser / remote-session guard (webbrowser.open returns
  True even for lynx/w3m over SSH) and returning whether a real browser opened.
- Consume the shared selectable_tiers / format_tier_row / is_upgrade helpers from the
  Free catalog, the paid picker, and the blocked-preview branch.
- /topup auto-refill copy: the concrete "charges $X … below $Y." sentence only when
  both amounts are present and finite; otherwise the generic sentence.

* docs(billing): correct CLI-parity rows (drop cross-repo ref, downgrade invariant)

Remove the other-repo PR reference from the manage-URL row, and state the real
downgrade invariant: a blocked downgrade may print the generic manage URL but never
carries plan=<tier_id> — selected-tier deep-links are reserved for new subscriptions
and upgrades.
2026-07-22 08:11:09 +05:30
brooklyn!
367810a942
Merge pull request #68857 from NousResearch/bb/theme-sdk
feat(themes): cross-surface theme SDK — one skin themes CLI, TUI, and desktop, live
2026-07-21 21:23:44 -05:00
brooklyn!
8f51376db3
Merge pull request #69040 from NousResearch/bb/fix-verify-candidate-warm-payload
fix(tui_gateway): candidate-inclusive display on warm/live + child-watch resume (#65919 fallout)
2026-07-21 21:16:38 -05:00
Brooklyn Nicholson
77855ce1f8 fix(tui_gateway): candidate-inclusive display on child-watch resume + E2E
Complete the #65919 warm/live-payload fix across its sibling path and add
real-SessionDB cross-builder coverage.

- Child-watch (lazy) resume: the delegated-subagent watch window served
  _history_to_messages(repaired_history) for its user-visible messages, which
  collapses out persisted verification candidates just like the warm-payload
  path did. Build the visible messages from the verbatim child-only display
  projection (repair_alternation=False) while the repaired history still feeds
  live replay; fall back to the repaired history if the display read fails.

- E2E cross-builder consistency (real SessionDB, not mocks): a persisted
  verification candidate is collapsed out of the model projection but kept in
  the display projection, and _live_visible_history now equals the eager
  session.resume display projection (candidate present). Adds the combined
  candidate + fully-flushed-second-turn case and a lazy child-watch handler
  test that asserts the candidate survives in resp["result"]["messages"].
2026-07-21 21:05:05 -05:00
Brooklyn Nicholson
850b8da332 fix(tui_gateway): serve candidate-inclusive display on warm/live resume
#65919 persists verification candidates (finish_reason=verification_required
/ verify_hook_continue) to state.db but collapses them out of the in-memory
model history via repair_message_sequence. The eager session.resume + REST
paths read the verbatim display lineage (candidate present), but the
warm/live-reuse payload (_live_session_payload) built its user-visible
messages from the collapsed in-memory model history — so switching to a
still-live session dropped the substantive verification answer that a cold
resume of the SAME session showed. That divergence is the cross-session
"substantive text vanishes on switch" class, and the direct sibling of the
resume-duplication regression fixed in #68149.

Reconcile the persisted display lineage (candidate-inclusive, the same
get_messages_as_conversation(..., include_ancestors=True) read the eager
resume + REST paths use) with the fresh in-memory tail in
_live_visible_history, so all three surfaces agree by construction while a
not-yet-flushed live turn is still shown. Extracted
_reconcile_display_with_live as a pure, DI-testable function (anchors on the
last persisted row's (role, text); appends only the uncovered in-memory tail;
trusts the DB display when the tail can't be anchored).

Tests: unit coverage for candidate-inclusion, freshness, empty/raising-DB
fallback, and the combined candidate+fresh-tail case. The existing freshness
guard (test_session_resume_live_payload_uses_current_history_with_ancestors)
stays green.
2026-07-21 21:02:10 -05:00
Brooklyn Nicholson
428a0534ee test(themes): E2E live skin switch — config write → skin.changed broadcast 2026-07-21 21:00:43 -05:00