- Extract the MoA @moa:/moa/ prefix-stripping branch of
_resolve_compatible_session_model_state into a new
_moa_fast_path_model_state() helper. Inlining it had pushed
`catalog = get_available_models()` just past the 6000-char
source window that
tests/test_issue1855_resolve_model_provider_fast_path.py::
TestFastPathSourceShape scans to guard the #1855 fast-path/
catalog-call ordering, breaking that regression test after
rebasing onto current master.
- Re-add gpt-4o to the refreshed Copilot static fallback list.
It's a real Copilot-served model and
tests/test_issues_373_374_375.py::TestStaleModelListCleanup::
test_copilot_list_unchanged asserts the Copilot list keeps it
even as #374 removes it from the generic OpenAI list. The live
Copilot catalog probe remains authoritative; this only affects
the cold-start/probe-miss fallback.
Verification:
./scripts/test.sh tests/test_issue1855_resolve_model_provider_fast_path.py tests/test_issues_373_374_375.py tests/test_issue5057_moa_webui_route.py tests/test_copilot_provider_model_settings_not_allowlist.py tests/test_moa_model_picker_provider.py tests/test_pr1970_lmstudio_base_url_fallback.py -q
-> 73 passed in 3.60s
./scripts/test.sh tests/ -k "config or provider or model or copilot or moa or lmstudio" -q
-> 1745 passed, 2 skipped, 9775 deselected, 1 xpassed in 68.57s
Maintainer review found regression 2 on commit 575996a7: the new
_has_provider_route gate (api/config.py) required api/base_url/api_key/key_env
before admitting a configured provider into the picker, which dropped a
models-only custom provider config (the lmstudio-style shape from #1970,
tests/test_pr1970_lmstudio_base_url_fallback.py::test_provider_catalog_preserves_dict_shaped_raw_key_lookup).
A naive fix (admit any config with models) would re-break
test_unknown_duplicate_copilot_provider_config_is_not_rendered, since a
spurious alias like copilot-2: {name: copilot, models: {...}} must stay
rejected.
Fix: admit a models-only provider config as evidence only when its canonical
id matches the active/configured provider (threaded via active_provider,
already in scope). Non-active models-only configs (including duplicate
aliases of known providers) are still rejected.
Added a regression test:
tests/test_pr1970_lmstudio_base_url_fallback.py::test_provider_catalog_rejects_non_active_models_only_custom_provider
covering both the active-admits and non-active-rejects cases side by side.
Verification:
./scripts/test.sh tests/test_issue5057_moa_webui_route.py tests/test_copilot_provider_model_settings_not_allowlist.py tests/test_moa_model_picker_provider.py tests/test_pr1970_lmstudio_base_url_fallback.py -q
26 passed, 1 skipped in 5.58s
Broader sanity pass: ./scripts/test.sh tests/ -k "config or provider or model or copilot or moa or lmstudio" -q
1728 passed, 5 skipped, 9654 deselected (4 pre-existing failures confirmed unrelated: 2 reproduce identically on the pre-fix commit via git stash, 2 are order-dependent and pass in isolation).
- Keep resolve_moa_preset optional so older hermes-agent installs still degrade gracefully.
- Surface MoA presets in the WebUI model picker as a virtual provider.
- Keep configured Copilot per-model settings from collapsing the built-in catalog.
- Refresh the Copilot static fallback used when the live catalog probe misses.
Verification:
./scripts/test.sh tests/test_issue5057_moa_webui_route.py tests/test_copilot_provider_model_settings_not_allowlist.py tests/test_moa_model_picker_provider.py -q
11 passed, 1 skipped
_session_is_subagent_view_only guard added to the remaining paths that could
mutate a stale persisted subagent sidecar:
- _handle_handoff_summary (appends tool msg to state.db)
- _handle_chat_sync (fallback POST /api/chat)
- /api/session/draft POST (composer_draft save)
- /api/personality/set (per-session personality save)
Fixes#5307
Enumerated every /api/session/<mutation> route and applied _session_is_subagent_view_only:
duplicate, branch, retry, undo, toolsets, compress, archive (existing-sidecar path)
now 400 for subagent children; delete/clear/truncate/pin already gated; rename/move/update
route through the gated _get_or_materialize_session (403). This closes the data-loss
paths (delete_cli_session, retry/undo truncate, branch/duplicate fork).
Also: /api/sessions list coercion now also honors state.db source='subagent' for rows
whose stale index says webui/fork, so they can't surface as writable/CLI sidebar rows.
Fixes#5307
The root non-CLI classification surfaced subagent rows in the sidebar without
read_only, exposing delete/truncate/pin — and /api/session/delete calls
delete_cli_session() which erases the child's state.db transcript (data loss).
- /api/sessions list: coerce subagent rows to read_only=True + is_cli_session=False
so the UI offers no mutation affordances.
- New _session_is_subagent_view_only() shared guard; applied to the direct
mutation routes that bypass _get_or_materialize_session(): delete / clear /
truncate / pin now 400 for subagent children. (rename/move already route
through the gated _get_or_materialize_session and 403.)
- tests: guard helper + static contract that all 4 routes + list coercion present.
Fixes#5307
- api/agent_sessions.py is_cli_session_row(): add 'subagent' to non_cli_sources
so EVERY consumer (sidebar rows, /api/sessions, etc.) classifies a delegated
child as non-CLI (the root the per-site fixes were compensating for).
- api/routes.py GET /api/session happy path: coerce is_cli_session=False +
read_only=True in the serialized payload for a subagent child before redaction,
so a stale writable sidecar can't be exposed as writable to the browser.
Fixes#5307
- _get_or_materialize_session happy path: reject an already-persisted subagent
sidecar (source_tag/raw_source=='subagent' or state.db subagent) even when it
was stored read_only=False (pre-fix materialization), so chat-start can't write it.
- import_cli existing-session refresh: coerce read_only=True on the persisted
sidecar (and response) when it's a subagent child.
- test: persisted read_only=False subagent sidecar is still refused by the helper.
Fixes#5307
The [Unreleased] section had accumulated 116 shipped feature/fix bullets spanning
100 releases (v0.51.693 → v0.51.792) — the release process bumped the version + tag
but never MOVED each PR's bullet out of [Unreleased] into a dated version block (it
stopped creating per-version blocks after v0.51.692). Marquee features (/moa, the
appearance skins, custom TTS engine, theme/skin registration, OIDC login, …) all sat
orphaned in [Unreleased], which is why the Discord announcement cron kept re-listing
the same items every run.
This moves every bullet into a dated version block reconstructed from its shipping
git tag (PR-number → first-containing-tag → tag commit date), grouped under the
original Added/Changed/Fixed subsections, and empties [Unreleased] to a
self-documenting placeholder. No bullet lost (116/116 redistributed, verified), no
new duplicate headers introduced.
Docs-only, no code change. Pairs with a release-process fix so this can't recur.
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
- import_cli _read_only_view now also treats resolved cli_meta source_tag/raw_source
=='subagent' as view-only (all_profiles=true resolves cli_meta from a non-active
profile, which the active-profile state.db _sa_child check could miss).
- import_cli existing-session refresh branch no longer hardcodes is_cli_session=True
for a subagent child (both the persisted update and the response payload).
Fixes#5307
Codex round 4 found POST /api/session/archive's missing-sidecar fallback also
calls import_cli_session(). grep confirms exactly 3 import_cli_session() call
sites in routes.py; all 3 now refuse source='subagent' children:
- 4792 _get_or_materialize_session (chat-start) -> PermissionError
- 22382 import_cli endpoint -> read-only view payload
- 13885 archive fallback -> 400 'Subagent sessions cannot be archived'
So no path can materialize a delegated child as a writable WebUI sidecar.
Fixes#5307
Codex found a 3rd writable path: POST /api/chat/start -> _get_or_materialize_session()
materialized source='subagent' as a writable sidecar before the not_claimable guard.
Fix: refuse subagent children (PermissionError) at that shared chokepoint, checked via
_is_subagent_child_session_id(sid) (state.db source, independent of cli_meta) BEFORE the
materialize path — so all three entry points (GET synth, import_cli, chat-start) now
consistently keep a delegated child view-only. CLI/TUI/Desktop materialization preserved.
Tests: materialize helper refuses subagent child + still allows tui.
Fixes#5307
- api/routes.py GET /api/session synthesized response: stop hardcoding
is_cli_session=True; serialize bool(synth.is_cli_session) so a recovered
subagent child stays not-CLI-classified (was overriding the helper's False).
- api/routes.py POST /api/session/import_cli: gate source='subagent' into the
read-only view payload (is_cli_session=False, imported=False) BEFORE
import_cli_session(), so a subagent child can never be materialized as a
writable WebUI sidecar via this endpoint.
- tests: assert import_cli routes subagent children read-only (no materialize).
Both were paths that bypassed the _is_claimable_cli_source denylist. Fixes#5307
Reworked per the Codex gate finding: the first approach widened the client
import predicate, which (a) conflicted with #3603's intentional _isExternalSession
gate and (b) let import_cli persist the subagent child as a WRITABLE, CLI-classified
session that then passed the poll-skip/active-refresh gates.
Corrected to a server-side, view-only recovery:
- api/routes.py: mark source='subagent' as NON-claimable in _is_claimable_cli_source
(both cli_meta and state.db source denylists). A subagent child now resolves via
the not_claimable branch -> read-only Session with its state.db transcript, and
build_session takes is_cli_flag (False for subagent children) so the recovered
session is NOT CLI-classified and can't widen the frontend _isExternalSession gates.
- static/sessions.js: REVERTED to master (no client change needed; #3603 contract intact).
- tests: assert reason=not_claimable, read_only=True, is_cli_session!=True, transcript present;
#2782 deleted-webui 404 preserved; #3603 _isExternalSession contract preserved.
58 tests green (5307 + 3603 + claim-cli + core-data-loss). Fixes#5307
A delegated subagent child (source='subagent' in state.db) usually has no WebUI
sidecar but is registered in the WebUI index sharing the parent's lineage. That
made GET /api/session -> _claim_or_synthesize_cli_session return 'was_webui' ->
404, so the child pane opened empty despite state.db holding messages.
- api/routes.py: add _state_db_session_source() + _is_subagent_child_session_id();
exclude subagent children from the was_webui 404 gate so they recover their
state.db transcript (the #2782 self-heal 404 for deleted WebUI sessions is kept).
- static/sessions.js: add _isSubagentChildSession() + _sessionNeedsServerImportForLoad()
(kept separate from _isExternalSession to avoid widening refresh-gating); the
main session-tap, lineage-segment, and child-open handlers now trigger the
import/merge path for subagent children.
- tests: test_5307_subagent_child_transcript.py (7 tests).
Fixes#5307
#5306 (flicker): while a parent WebUI session is the active/streaming session,
a linked delegate subagent child that transiently reports message_count===0
between /api/sessions polls was dropped by _sidebarRowHasVisibleMessages BEFORE
_attachChildSessionsToSidebarRows could stack it under its parent. It never
entered sessionsRaw, so the row vanished, then reappeared on the next refresh
once its list metadata caught up — the flicker. Extend the visibility predicate
with an active-parent exception (mirroring the existing active-session
exception): a child_session whose parent_session_id is the active sidebar
session stays visible even at message_count 0. Scoped to the active parent so
truly-empty unrelated sessions are still hidden.
#5305 (orphan): a delegated subagent child whose WebUI parent is filtered out of
the current render (project/profile/source scope) was promoted to a contextless
top-level "Subagent Session" orphan. Suppress cross-surface child_session rows
whose parent row is absent from the render instead of orphaning them, mirroring
the existing archived-hidden-parent suppression (#4293). The genuinely-external
parent case (messaging/CLI) still orphans via the parentIsExternal branch.
Tests: tests/test_5306_subagent_sidebar_flicker.py (7 tests) locks both
invariants and the no-regression guards, executing the real sessions.js helper
regions under node like the existing lineage tests.
The agent source at /opt/hermes inside the container may contain a
.playwright/ directory with browser dependency files that have restricted
permissions. rsync fails with exit code 23 when attempting to read them,
which kills the container build ("Failed to stage hermes-agent source").
- Add --exclude=.playwright to rsync in docker_init.bash
- Add rm -rf .playwright to cp -a fallback path for symmetry
- Update test_docker_init_excludes_egg_info_during_staging to assert
both the rsync --exclude and a broad .playwright presence check
- Add a brief note in AGENTS.md Contribution style about mirroring
directory exclusions in both rsync and cp paths
Fixes#5315
Reapply footer control ordering on current upstream/master while preserving the required situational chip renderer. Persist composer_control_order with backend validation, keep the settings descriptions reorder-aware, and make primary/situational chip renderers participate in same-group drag ordering.
Verified with: node --check static/boot.js; node --check static/panels.js; git diff --check; ./scripts/test.sh tests/test_issue4598_composer_control_visibility.py