36 Commits
Author SHA1 Message Date
nesquena-hermes 262236147e changelog: /skills write-approval fall-through (#7623) + disabled-skill picker filter (#7511) 2026-09-18 03:00:14 +00:00
nesquena-hermesandwebtecnica 9c1413ab9d fix(commands): keep disabled skills out of the slash picker, fenced across profile switch (#7511)
Disabled skills are excluded from the backend skill-command map, but the WebUI
picker listed every /api/skills entry regardless of skills.disabled -- so a
disabled skill stayed selectable in both '/use <skill>' sub-args and the
skill-command suggestions (#7509). A single _isSkillDisabled() gate now filters
both surfaces.

Also fences the skill caches across a profile switch: invalidateSlashSkillCaches()
bumps a generation counter that an in-flight /api/skills reply checks before
committing, so a payload generated for the outgoing profile can no longer land
after the switch and keep hiding a skill that is enabled in the new profile.

Maintainer follow-up folded in: loadSkillCommands() published readiness in a
finally block that ran even when /api/skills rejected, leaving
_skillCommandCacheReady=true with an empty cache. Because
ensureSkillCommandsLoadedForAutocomplete() only re-loads while
!ready && !promise, one transient failure PERMANENTLY emptied the slash-command
picker until a page reload. Readiness is now published only after a successful
current-generation commit. This wedge is PRE-EXISTING on master (its finally sets
readiness unconditionally) and is not introduced by this PR; fixed here because
this change already reworks the surrounding generation handling. Added a
reject-once/recover regression plus a __failSkillsOnce() harness knob.

Gate: Codex SAFE TO SHIP on the fixed head — confirmed the wedge is closed for
every failure interleaving, the stale-generation fence is NOT weakened (a stale
rejection landing after a fresh success leaves the fresh cache intact), and
_skillCommandCacheReady=_committed is correct rather than ||-ing with the old
value (which would re-create the wedge after a forced refresh erased the cache).
Verified the API really emits a boolean 'disabled' per skill (api/routes.py:931)
so the predicate matches the true payload. Full suite 15,612 passed with only the
known box-baseline singleton; 9/9 own tests, 34/34 neighboring slash tests;
16/16 required checks green.

Co-authored-by: webtecnica <webtecnica@users.noreply.github.com>
2026-09-18 02:58:59 +00:00
nesquena-hermesandtotalitarian 3cc4033353 fix(webui): let /skills write-approval subcommands reach the agent (#7623)
The local /skills handler swallowed every argument into its own skill-name
search, so the agent-owned write-approval subcommands never reached
hermes_cli/write_approval_commands.py -- '/skills pending' and friends silently
ran a search instead. cmdSkills() now returns false for those subcommands, using
the pre-existing documented opt-out contract honored at commands.js:180 (the same
one /reasoning already uses), so they fall through to the normal send path.

Maintainer follow-up folded in: the submitted list covered 6 subcommands but the
agent handler also accepts the aliases apply, deny and drop, so '/skills apply 3'
and '/skills deny 3' were still being swallowed. Completed the class against the
live handler's alias sets and verified all 18 alias/query cases route correctly.

Gate: node -c + ESLint runtime gate clean; 16/16 required checks green.

Co-authored-by: totalitarian <totalitarian@users.noreply.github.com>
2026-09-18 02:58:42 +00:00
nesquena-hermesandwebtecnica a89808886f fix(commands): do not latch an empty skill-command cache after a failed load
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>
2026-09-18 02:39:35 +00:00
Hermes Agent b7f30c6365 perf(webui): memoize the sensitive-text prefilter
Once large strings stopped being re-redacted on every response, cProfile
showed the cheap prefilter had become the dominant cost of the redaction
pass: 2.98s of 3.7s, spent testing 71 literal markers against each of the
~74k strings (~19MB) of a large session, i.e. ~3.1M interpreted iterations
per request — repeated by every tab and every poll for a constant verdict.

Memoize the verdict behind a bounded LRU, mirroring the existing redaction
caches: entry count and per-entry size are both capped, so retained bytes
stay bounded and oversized blobs simply run the scan uncached. Non-strings
never reach the cache, so an unhashable value cannot raise.

Measured on a real 22MB session, warm: full redaction 2.07s -> 1.07s (-49%),
output byte-identical. Combined with the large-string cache, the recurring
redaction cost drops from ~8.4s to ~1.1s.

Rejected alternative, kept in a comment: collapsing the 71 `in` scans into
one compiled alternation is the obvious-looking fix but measured 38% SLOWER
(2.60s vs 1.88s) — CPython's substring search is already a tuned C routine.

Security: the prefilter gates the entire redaction pass, so equivalence is
pinned by a differential test suite against a copy of the previous
implementation — every marker individually, case variants, embeddings,
near-misses, unicode lowercase traps, 4000 fuzz cases, plus oversized and
unhashable inputs. Verdicts are identical on all 73,658 strings of a real
session (1935 flagged before and after).

Broad selection: 22 failures before and after, identical set — all
pre-existing.
2026-09-17 23:21:46 +00:00
Hermes Agent 6f05ff9657 perf(webui): stop re-redacting large strings on every response
`_redact_fn_cached` skipped memoization for any string over 16KB, so those
strings re-ran the full ~15-pass redactor on every single request. On a real
22MB session that is 59 large strings (29 unique, 0.76MB) costing 1.68s per
request — 99.9% of the recurring redaction cost once the small-string cache is
warm — repaid by every tab and every poll for a byte-identical result.

Large strings now get their own bounded cache, separate from the 4096-entry
small-string one so a large blob can never evict the thousands of small
recurring strings. Entry count and per-entry size are both capped; anything
above the ceiling stays uncached, keeping retained bytes bounded.

`_redact_value` additionally stops rebuilding containers whose descendants
were unchanged. Most of a transcript carries no credential marker, so the
unconditional dict/list comprehension deep-copied the whole payload to
reproduce an identical structure — pure CPU under the GIL, which serializes
concurrent tab loads on a threaded server. Callers treat redacted output as
read-only (`_public_message_projection` and `redact_session_data` both build
fresh containers), so sharing an unmodified subtree cannot leak a later
mutation back into session state. As soon as one string is masked, every
container on the path to it is rebuilt and the caller's original is untouched.

Measured on the live 22.6MB session, steady state: redact_session_data 3.78s
-> 1.96s single tab (-48%), 7.93s -> 4.06s with two concurrent tabs (-49%),
byte-identical output.
2026-09-17 23:21:46 +00:00
nesquena-hermes 4ba80ced12 changelog: AGENTS.md task-scoped pointers (#7593) + /api/models cache contract doc (#7560) 2026-09-17 23:01:06 +00:00
nesquena-hermesandwebtecnica 8861531dee docs(architecture): document the /api/models cache invalidation contract (#7560)
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>
2026-09-17 23:00:04 +00:00
nesquena-hermesandsteveafrost e07606a6b2 docs(agents): scope documentation pointers by task and state completion criteria (#7593)
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>
2026-09-17 22:59:52 +00:00
nesquena-hermesandtotalitarian a865c7f688 fix(webui): also fall through the approve/reject/approval aliases
The agent's handle_pending_subcommand() accepts aliases the initial list
missed: approve|apply, reject|deny|drop. Those three were still swallowed by
the local /skills search, so '/skills apply 3' and '/skills deny 3' silently
ran a skill-name search instead of reaching the write-approval handler.

Completes the class rather than the reported instance.

Co-authored-by: totalitarian <totalitarian@users.noreply.github.com>
2026-09-17 22:56:36 +00:00
nesquena-hermes 16ebfcace8 changelog: zip media delivery (#7612) + Agent moved-name resolution (#7574) 2026-09-17 21:58:48 +00:00
nesquena-hermesandmikemchargue 818c392ed4 fix: resolve Hermes Agent names moved by the Sep 2026 decomposition (#7574)
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>
2026-09-17 21:57:25 +00:00
nesquena-hermesandcarlotestor 8e100d0540 fix(media): serve .zip archives through /api/media (#7612)
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>
2026-09-17 21:40:23 +00:00
nesquena-hermesandJayaMegelar eff4900fa8 test(issue342): widen renderMd body window so autolink assertion survives added lines
The structural test_autolink_regex_in_rendermd slices renderMd's body with a
hardcoded 15000-char window. This PR's <br>-in-table protection adds ~315 chars
near the top of renderMd, pushing the autolink pattern from offset 14785 (master)
to 15100 — just past the window. The autolink regex is unchanged (12 occurrences
remain in ui.js); the test window was simply too tight. Widen to 30000.

Co-authored-by: JayaMegelar <JayaMegelar@users.noreply.github.com>
2026-09-17 05:14:47 +00:00
nesquena-hermes 0efa75827a fix(streaming): pass config overrides into context-length fallback (#1896)
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>
2026-05-08 16:08:42 +00:00
nesquena-hermes f456daa574 fix(streaming): include profile home in agent cache signature (#1897)
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>
2026-05-08 16:08:18 +00:00
Hermes Agent 4f50cb2511 Reference correct issue number (#1431) in comment + CHANGELOG 2026-05-01 23:47:46 +00:00
Hermes Agent 4adbb5ebee Hide composer-footer toolsets chip (cramped layout)
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).
2026-05-01 23:47:13 +00:00
Hermes Agent 2a0757fb46 v0.50.253: CHANGELOG entry for the batch (#1342 + #1388 + #1381 + 2 Opus follow-ups) 2026-05-01 06:54:30 +00:00
Hermes Agent 67193faf38 Apply Opus pre-release follow-ups for v0.50.253
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.
2026-05-01 06:53:32 +00:00
Hermes Agent 8cd3680c0c Absorb starship-s commit cddd175: tighten composer spacing on 320px legacy phones
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.
2026-05-01 06:15:13 +00:00
Hermes Agent 1c356bf321 PR #1381 fix: prevent mobile-config-btn from leaking into desktop view (CSS specificity)
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
2026-05-01 05:49:30 +00:00
Hermes Agent 1a76e8761e Mobile composer layout: progressive-disclosure config panel + scoped titlebar safe-area (#1381) 2026-05-01 05:36:59 +00:00
Hermes Agent 18d960eb7a Revert PR #1342's test_issue1195 rewrite — del sys.modules corrupts shared module state
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.
2026-05-01 05:35:24 +00:00
Hermes Agent 52bfceaa3b Add /branch command to fork conversations from any message (#1342, fixes #465)
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.
2026-05-01 05:32:45 +00:00
Hermes Agent fea47bd986 Heal 'provider: local' mid-conversation crash for local-model users (#1388, fixes #1384) 2026-05-01 05:29:42 +00:00
Hermes Agent afc68d3a13 CHANGELOG: document Opus pre-release follow-ups for v0.50.252 2026-05-01 05:07:25 +00:00
Hermes Agent fc8898161e Apply Opus pre-release follow-ups (force redaction, log profile fallback) 2026-05-01 05:07:09 +00:00
Hermes Agent 6572f81abb Add v0.50.252 changelog entry (6 contributor PRs) 2026-05-01 05:04:15 +00:00
Hermes Agent 2bc6f9a997 Add regression test for #1386 (model='unknown' init guard) 2026-05-01 04:48:24 +00:00
Hermes Agent e36def33cd Show profile home in /status command (refs #463) (#1380) 2026-05-01 04:46:37 +00:00
Hermes Agent 5c5ca7d2ef Intercept CLI-only slash commands in WebUI (#1382) 2026-05-01 04:46:30 +00:00
Hermes Agent 838b931047 Keep API credential fallback redaction active (#1379) 2026-05-01 04:46:17 +00:00
Hermes Agent a6d831fc63 Cache /api/models/live with 60s TTL (#1378) 2026-05-01 04:46:15 +00:00
Hermes Agent d21c97205e Harden streaming scroll unpin behavior (#1360) (#1377) 2026-05-01 04:46:12 +00:00
Hermes Agent d1e1c4eeec Fix CLI session import fallback model default (#1386) 2026-05-01 04:46:10 +00:00