loadSkillCommands() published readiness in a finally block that ran even when
/api/skills rejected, so one transient failure left
_skillCommandCacheReady=true with an empty cache. Because
ensureSkillCommandsLoadedForAutocomplete() only re-loads while
!_skillCommandCacheReady && !_skillCommandLoadPromise, no later picker pass ever
retried: skill commands stayed missing until a page reload even after the API
recovered.
Readiness is now set only after a successful current-generation commit; the
promise is cleared either way so the next pass can retry. The stale-generation
fence is unchanged (verified a reply from the outgoing profile still refuses to
commit).
This wedge is PRE-EXISTING on master (its finally sets ready unconditionally),
not introduced by this PR — fixed here because this change already reworks the
surrounding generation handling. Adds a reject-once/recover regression plus a
__failSkillsOnce() harness knob; reverting the fix turns it red.
Co-authored-by: webtecnica <webtecnica@users.noreply.github.com>
Records the shipped identity and invalidation contract for the /api/models
catalog cache in api/config.py — what is cached (in-memory snapshot, per-profile
models_cache.json, cold vs hot path), the three source axes and why each is
fingerprinted the way it is, and the one-directional deny-list invariant for the
volatile-key sets. Changes no runtime behavior; added after #7556 shipped, whose
review flagged the contract as undocumented.
Verified before merge: all 14 symbols the doc names exist in api/config.py
(_available_models_cache, _AVAILABLE_MODELS_CACHE_TTL, _get_models_cache_path,
_models_cache_provenance, _sync_models_cache_provenance,
_models_cache_source_fingerprint, _is_loadable_disk_cache,
_models_cache_file_fingerprint, _auth_store_semantic_fingerprint,
_AUTH_FINGERPRINT_VOLATILE_KEYS, _codex_models_cache_fingerprint,
_CODEX_CACHE_FINGERPRINT_VOLATILE_KEYS, _endpoint_advertised_model_ids), and the
cited test_unknown_codex_field_stays_in_fingerprint is real
(tests/test_issue7540_codex_catalog_fingerprint.py:287). docs/architecture/ is
allowlisted in .gitignore with both the dir and /** lines, so the new file is
genuinely trackable.
Co-authored-by: webtecnica <webtecnica@users.noreply.github.com>
Restructures AGENTS.md so contributors read the references that match their
change instead of a blanket four-file reading list, and states explicit
completion/verification criteria in place of the compressed ten-rule summary.
Verified before merge: all 12 referenced paths exist on master (README,
CONTRIBUTING, docs/CONTRACTS.md, docs/GUIDELINES.md, CHANGELOG, ARCHITECTURE,
TESTING, docs/onboarding.md, docs/troubleshooting.md, docs/rfcs/README.md,
docs/UIUX-GUIDE.md, DESIGN.md), the referenced ./scripts/test.sh exists, and
the ten rules removed from AGENTS.md are all preserved in docs/GUIDELINES.md —
which the new version still points to. Net -2 lines, so no information is lost.
Co-authored-by: steveafrost <steveafrost@users.noreply.github.com>
The Agent's September 2026 decomposition moved several names the WebUI uses into
sibling modules. The old import paths resolved only through PEP 562 __getattr__
compat pointers whose removal date (2026-09-14) has already passed. Every affected
WebUI call site is wrapped in a broad except, so once those pointers go away nothing
crashes -- features silently degrade: MCP tools vanish from turns, the per-turn
approval session key never binds, Claude Code credential linking stops, and the
kanban bridge raises AttributeError.
Adds api/agent_compat.py::agent_attr(owner, name, home, default), which resolves a
moved name via the original module's own namespace (pre-split Agents and test stubs),
then the new home module (split Agents, with or without the pointers), then plain
attribute access -- and routes the 8 affected call sites through it.
Gate: Codex SAFE TO SHIP. All 12 mappings verified against the installed Agent's
authoritative compat_manifest.json; owner-wins precedence is correct because PEP 562
pointers do not populate vars(owner), so current pointers are bypassed without
warnings; the only explicit defaults are the pre-existing optional kanban fallbacks
and remain semantically equivalent; turn-approval identity, MCP discovery/reload/status,
OAuth credential reading, LM Studio probing, and all three kanban paths preserve their
previous values and control flow; cached import_module costs ~1.36us and the hottest
site is a 0.3s kanban poll, not per-token streaming (caching the callable would break
supported module replacement/monkeypatching). Tests are non-vacuous across pre-split,
pointer-present, and pointer-removed shapes. Full suite 15,597 pass with only the known
box-baseline singleton. Maintainer proof: agent_attr resolves the moved name with zero
HermesPluginCompatWarning while a clean-master control still resolves via the pointer
and warns.
Co-authored-by: mikemchargue <mikemchargue@users.noreply.github.com>
Archives resolved to application/octet-stream (.zip absent from MIME_MAP) and were
therefore rejected by the session MEDIA: token allow-list, so a MEDIA:/path/file.zip
link always failed to open. Adds .zip -> application/zip and admits archives to the
session-token type set. Archives are download-only by construction: _ARCHIVE_TYPES is
never added to the inline-preview sets, so they always get Content-Disposition:
attachment.
Gate: Codex SAFE TO SHIP (disposition verified incl. inline=1 and uppercase .ZIP;
confinement requires exact canonical path equality vs a non-user MEDIA: token; the
#3234 state/secret deny guard runs before the allow decision and reproduced a 403 on a
token-authorized backups/state-backup.zip; traversal and workspace-symlink both 403;
.docx/.xlsx mappings intact). Full suite 15,580 pass with only the known box-baseline
singleton. Maintainer added a live behavioral proof (the PR's own tests are source-string
assertions only); it failed on clean master and passed on this head.
Co-authored-by: carlotestor <carlotestor@users.noreply.github.com>
The two get_model_context_length() fallback callsites in api/streaming.py
(session save + SSE usage payload) were calling the resolver with only
model + base_url. When the agent's compressor reports 0 (fresh/cached/
transitioning agent), resolution fell through to the 256K DEFAULT_FALLBACK
even when users had set model.context_length: 1048576 in config.yaml.
For LCM users on 1M-context models, the wrong window cascaded into a
session-killing failure: auto-compression triggered at ~25% of the wrong
value, floods of compress requests, 429s, credential pool exhaustion,
fallback 429s, then 'API call failed after 3 retries'.
Reported by @AvidFuturist on Discord with deepseek-v4-flash. Reproduced 5x.
Both callsites now pass config_context_length, provider, and
custom_providers. The resolver consults these BEFORE probing, so the
config override wins. Both are wrapped in except TypeError blocks that
retry with the legacy 2-arg form for older hermes-agent builds whose
get_model_context_length signature pre-dates these kwargs.
Tests: 7 source-string regressions guarding both call shapes, the safe
config parse, the legacy fallback, and the per-profile config source.
Also bumped the line-distance assertion in test_pr1341 (the test
explicitly invites bumping when a new pre-save mutation block is added).
Closes#1896
Co-authored-by: Hermes Agent <agent@hermes.local>
Same-session profile switches reused cached AIAgent from previous profile,
silently leaking the old persona's SOUL.md / system prompt into the new
profile's turns. session_id stays stable across profile switches, and the
signature didn't include the active profile home, so every signature input
matched and the stale agent was returned from SESSION_AGENT_CACHE.
Append _profile_home to the signature blob so profile switches force a
cache miss and a fresh agent build under the new HERMES_HOME (which
triggers a fresh load_soul_md() call).
Tests: 3 source-string regressions guarding the signature contract,
ordering, and empty-home fallback.
Closes#1897
Co-authored-by: Hermes Agent <agent@hermes.local>
The session-toolsets restriction chip (#493) was making the composer
footer too cramped on narrower widths once it was sharing space with
model, reasoning effort, profile, and context-usage indicators.
Surgical fix: `_applyToolsetsChip()` now sets the wrap to display:none
unconditionally. Underlying state and the /api/session/toolsets endpoint
still work, so any cron job or scripted client that relies on
`enabled_toolsets` continues unaffected. To be revisited when the
footer layout is redesigned (#1430).
Three small fixes from Opus review of the merged stage diff:
1. Strip 9 orphan wiki_* i18n keys (72 lines) from PR #1342 — leaked
from a different branch, zero references outside i18n.js.
2. /branch endpoint: reject non-string session_id with explicit 400
(was raising TypeError → generic 500 from get_session()).
3. /branch endpoint: reject negative keep_count with explicit 400
(Python slice semantics on negative produces 'all but last N',
confusing fork behavior).
Plus tests/test_v050253_opus_followups.py — 3 regression tests pinning
all three fixes.
Verified: 3558 pytest passing.
Pulls in the extra commit pushed to PR #1381 after our initial absorb. Adds a
@media (max-width: 340px) block that compacts gutters (composer-wrap padding,
composer-footer gap, composer-left gap) without shrinking the 44px touch
targets. Plus its regression test.
Verified with apply --check failed but actual apply succeeded — the failure
was due to context drift from our earlier CSS specificity fix; the new lines
landed at the correct location. test_mobile_layout.py: 47 tests passing.
The .composer-mobile-config-btn{display:none} base rule was at line 896 but
.icon-btn{display:flex} (the button's other class) was at line 941 — equal
specificity, but later in source wins. Result: the button was visible at
desktop widths, sandwiched between the workspace and model chips.
Bumping the base rule's selector to .icon-btn.composer-mobile-config-btn
gives it specificity 0,0,2,0 (vs .icon-btn at 0,0,1,0), so it always wins
the cascade. The two narrow-viewport rules already use !important and remain
unaffected — desktop hides cleanly, mobile shows correctly.
Verified via Agent Browser CDP: 1440x900 desktop now shows the standard
chips only (no extra config button); iPhone 14 mobile shows the new compact
config btn at 44x44 with the panel toggling correctly. Screenshots:
/tmp/may2-shots/desktop-final.png, mobile-{closed,open}-final.png
PR #1342's rewrite introduced `del sys.modules['api.config']`, 'api.profiles']`
anti-pattern that breaks tests/test_live_models_ttl_cache.py::test_live_models_cache_is_profile_scoped
(v0.50.252) when run after test_issue1195_*. The pattern is explicitly banned per
~/WebUI/docs/agent-memory/pytest-isolation.md — sibling tests that import api.profiles
later see the wrong (re-imported) module.
Master's version of this test passes 5/5 and uses no del sys.modules calls. The PR's
core /branch feature does NOT depend on this test rewrite — reverting it loses no
coverage of the branching feature.
Fix: gate parent_session_id emission in compact() on truthiness so
sessions without a fork link don't leak parent_session_id: None and
break the v0.50.251 lineage end_reason gating in agent_sessions.py.
The /branch endpoint sets the field on saved forks; everything else
keeps the v0.50.251 sidebar lineage path as the canonical source.