- Add test_profile_model_selection_accepts_model_in_overflow_extra_models
(the extra_models overflow-bucket regression test the warm dossier flagged
as the #5453 ship-pass note).
- Drop the stray dead pseudo-code comment left in handle_post branch route
(#5449 cosmetic nit).
read_importable_agent_session_rows() is a pure read, but it opened a
read-write sqlite connection on the live multi-GB WAL state.db and re-ran a
defensive CREATE INDEX self-heal on every sidebar build. Holding a
write-capable handle while the agent streams into the same DB adds needless
checkpoint/lock surface.
Open the DB read-only (file:...?mode=ro) for the projection, and self-heal a
missing idx_messages_session through a separate short-lived writable
connection only. With the index present (the normal case) the read path
performs zero writes; when it is missing the self-heal still runs and rows
still come back.
Behavior-neutral: identical rows when the index exists; self-heal preserved
when missing; read-only-fs degradation to the pre-aggregated path unchanged.
Tests: tests/test_issue5455_listing_readonly_connection.py (read-only open,
no writable connection when index present, self-heal when missing).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Rebase PR #5162 (rename busy_input_mode -> default_message_mode; flip the
default from 'queue' to 'steer') onto current origin/master WITHOUT dropping
the shipped #5170 localStorage persistence mirror.
Rename the mirror machinery to the new setting name for consistency:
_BUSY_INPUT_MODES -> _DEFAULT_MESSAGE_MODES (values unchanged)
_normalizeBusyInputMode -> _normalizeDefaultMessageMode (fallback now 'steer')
_persistBusyInputMode -> _persistDefaultMessageMode
_readPersistedBusyInputMode -> _readPersistedDefaultMessageMode
window._busyInputMode -> window._defaultMessageMode (+ renamed exports)
localStorage: write the new 'hermes-default-message-mode' key; read it with a
fallback to the legacy 'hermes-busy-input-mode' key so an existing user's
persisted preference survives the rename.
Preserve #5170 behavior at every mirror site under the new names:
- boot success -> window._defaultMessageMode=_persistDefaultMessageMode(...)
- boot FAILURE -> window._defaultMessageMode=_readPersistedDefaultMessageMode()
(NOT a hardcoded 'steer' — a saved 'interrupt'/'queue' must still apply when
the server is unreachable; do not regress #5167/#5132)
- preferences autosave, settings-panel load, and _applySavedSettingsUi all
persist through _persistDefaultMessageMode(...)
Tests updated for the rename while keeping the persistence-behavior assertions
(test_1062, test_5145, test_5167); test_5167 gains explicit guards that the
load-failure path reads the persisted pref and never hardcodes a literal mode,
plus autosave/panel-load mirror-write coverage.
Co-authored-by: Rod Boev <rod.boev@gmail.com>
- _repair_bare_custom_provider_model matches display-named providers via
_custom_provider_slug_from_name (custom:my-proxy matches 'My Proxy').
- _ordered_custom_provider_model_ids now handles dict keys, list strings,
and list dicts with id/model/name, aligned with api/config.py catalog.
- config_obj=None fallback uses get_config() instead of raw cfg.
- _read_profile_model_config returns profile config dict too, avoiding a
second YAML parse on hot display paths.
- Tests added for slug matching, list-form models, and get_config fallback.
Co-authored-by: b3nw <b3nw@users.noreply.github.com>
The two-tier mtime cache from #4783 fixed the per-request SKILL.md rescan
but left two concurrency holes that only bite at container cold start,
when the frontend fires several profile-data requests at once and the
caches are empty:
1. `_get_profile_skills_stats()` had no lock, so concurrent misses on the
same profile each ran `os.walk(followlinks=True)` + parsed every
SKILL.md simultaneously.
2. `_build_profile_rows_fast()` ran outside `_LIST_PROFILES_CACHE_LOCK`
in `list_profiles_api()`, so every concurrent request rebuilt all rows
(each walking every profile's skill tree) at once.
With ThreadingHTTPServer (one OS thread per request) and Docker overlay2,
this stacked thousands of concurrent stat() calls and stalled workers
57-70s (per the report's thread dumps).
Fix:
- Add a per-profile compute lock (registry guarded by a meta-lock) and
use double-checked locking in `_get_profile_skills_stats()`: concurrent
misses on one profile collapse to a single compute, while independent
profiles still compute in parallel.
- Single-flight the row build in `list_profiles_api()` by holding
`_LIST_PROFILES_CACHE_LOCK` across the build + cache write. Lock order
is strictly list-lock -> per-profile skills-lock, so no deadlock.
The report's third suggestion (debounce the mtime probe) is deliberately
NOT taken: the every-call cheap probe is the #4783 out-of-band
change-detection contract (test_issue4783 asserts it MUST run on every
call). Serializing the misses removes the herd without weakening that
contract, since only the expensive compute is guarded, not the probe.
Adds tests/test_issue5364_skills_stats_thundering_herd.py proving the
herd collapses (single compute / single build under a concurrent burst),
independent profiles still parallelize, and the every-call probe contract
is preserved. All existing #4783 contract tests still pass.
Co-authored-by: claw-io <claw-io@users.noreply.github.com>
_session_message_content_key (the state.db reconciliation key in
api/models.py) normalized whitespace only, while the streaming-side
identity _message_identity strips the workspace prefix for user turns.
WebUI sends the model a workspace-prefixed user_message
([Workspace::v1: /path]\n<text>) while the visible/optimistic bubble and
sidecar row carry the bare <text>. The mismatch made a prefixed state.db
row and a bare sidecar row key differently, so state_db_delta_after_context
failed to align them, treated the state.db copy as new, and appended a
duplicate user turn. The agent-side merge then concatenated the two
adjacent user rows into a permanent composite -- the post-restart
stale-user-prepend bug.
Fix: strip the workspace prefix for role=='user' in the reconciliation
key, reusing the same _strip_workspace_prefix helper the streaming side
uses (lazy import to avoid the api.streaming -> api.models cycle) so the
two dedup layers can't drift again. Assistant/tool keys are unchanged and
prefix-free user messages key identically (idempotent).
Fixes#5339.