Commit Graph
8164 Commits
Author SHA1 Message Date
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
webtecnica 65e968c447 fix(commands): drop slash-skill caches on profile switch
The slash-skill caches (_slashSkillCache, _slashSkillCachePromise,
_skillCommandCache) outlived a profile switch, so the outgoing profile's
/api/skills payload kept hiding skills that the incoming profile enables
(#7509).

- call invalidateSlashSkillCaches() from both switch handlers
  (static/panels.js switchToProfile, static/sessions.js
  _switchProfileForSessionLoad), after the /api/profile/switch request
- bump a generation counter in invalidateSlashSkillCaches() and capture it
  in both loaders: a reply issued for the outgoing profile can no longer
  commit once the switch has landed
- tests: in-flight race and cache-refresh regression on both picker
  surfaces, plus a check that both switch paths call the invalidation
2026-09-18 02:23:23 +00:00
webtecnica 9cb0235f5e fix(commands): keep disabled skills out of the slash-command picker
`skills.disabled` entries are already excluded from the backend skill-command
map, but the composer still offered them as selectable slash commands and as
`/use` sub-args: both picker surfaces read `/api/skills` and ignored the
`disabled` flag the endpoint reports per skill.

Add one `_isSkillDisabled()` gate in static/commands.js and apply it at the two
places that turn `/api/skills` rows into completions:

* `_buildSkillCommandEntry()` returns null for a disabled skill, which keeps
  `_skillCommandCache` (and every consumer of it, including the dropdown) clean;
* `_loadSlashSkillSubArgs()` skips disabled skills when building `/use`
  sub-args.

Both caches still rebuild from `/api/skills`, so a skill toggled off in the
skills panel disappears from the picker on the next load.

Adds tests/test_issue7509_disabled_skills_slash_picker.py, which runs the real
static/commands.js in a vm context with a mocked `api()` and asserts on
`getSlashAutocompleteMatches()` for both surfaces, over 0 / 1 / many skills,
enabled / disabled / flag-absent entries and a disable-then-refresh cycle.

Fixes #7509
2026-09-18 02:23:23 +00:00
nesquena-hermes 2cf8e8a5ea Merge pull request #7626 from nesquena/release/stage-317
Release exp-v0.52.317: documentation accuracy (#7593, #7560)
2026-09-17 16:04:20 -07: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-hermes 77323c310e Merge pull request #7624 from nesquena/release/stage-316
Release exp-v0.52.316: zip media delivery (#7612) + Agent moved-name resolution (#7574)
2026-09-17 15:23:59 -07: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
Mike McHargueandClaude Opus 5 8abf95335b test+docs: exercise moved-name call sites; document the agent_compat contract
Review follow-up for the Hermes Agent moved-name fix.

Tests: replace the AST source guard with behavior tests that run each
affected WebUI entry point against two fake Agent shapes, pre_split (name on
the original module) and pointer_removed (name only in its new home):
approval session identity bind/reset, MCP runtime status, /reload-mcp,
Claude Code credential read, LM Studio reasoning options, and kanban
connections (closed after use), board counts, SSE event feed, and dispatch.
All 8 pointer_removed cases fail against the pre-fix call sites; the
pre_split cases pass on both.

Docs: ARCHITECTURE.md gains the api/agent_compat.py inventory entry and
section 4.9 covering ownership (Agent's compat_manifest.json), resolution
order, call-site rules, and the removal policy.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 21:26:50 +00:00
Mike McHargueandClaude Opus 5 1f7e5e78da fix: resolve Hermes Agent names moved by the Sep 2026 decomposition
Hermes Agent moved several names the WebUI uses into sibling modules
(tools.approval_context, tools.mcp_tool_discovery/_lifecycle,
hermes_cli.kanban_db_connect/_dispatch, hermes_cli.models_local,
agent.anthropic_credentials). The old paths only resolved through
PLUGIN-COMPAT __getattr__ pointers that warn on every use and are removed
on schedule by reverting one commit.

With the pointers removed, every affected WebUI call site is wrapped in a
broad except, so nothing crashes; features silently degrade instead:
per-turn approval session-key binding is skipped, MCP discovery/status
return nothing, /reload-mcp reports "MCP runtime unavailable", Claude Code
credential linking returns None, every kanban connection raises
AttributeError, and the dispatcher reports "unavailable".

Add api.agent_compat.agent_attr, which resolves a moved name from the
original module's own namespace first (pre-split Agents and existing test
stubs), then from its new home (split Agents, with or without pointers),
then plain attribute access (non-module doubles). Importing only the new
modules would have broken pre-split Agents and bypassed the sys.modules
stubs the test suite installs on the original modules.

Names still native to their original module (_servers, _lock,
has_blocking_approval, is_session_yolo_enabled, register/unregister
gateway notify, the rest of kanban_db) are unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 21:26:50 +00:00
carlotestor 8c5ee95dab fix(media): serve .zip archives through /api/media
MEDIA:/path/to/file.zip links always failed to open in chat. .zip was
absent from MIME_MAP, so it resolved to application/octet-stream, which
_SESSION_MEDIA_TOKEN_TYPES rejects — the browser rendered a download
link that the server then refused.

Map .zip to application/zip and add an _ARCHIVE_TYPES set to the session
MEDIA: token allow-list. Archives are deliberately kept out of
_INLINE_PREVIEW_TYPES and _INLINE_IMAGE_TYPES so they are always sent as
Content-Disposition: attachment and never rendered inline.
2026-09-17 20:56:30 +00:00
nesquena-hermesandn 06d28c08cb Release: make composer single-row autoResize fast path reachable (#7604) (#7607)
Co-authored-by: n <a@n>
2026-09-16 11:17:36 -07:00
Kevin Meinon be5ee8f503 perf(composer): make the single-row autoResize fast path reachable (long-session typing lag) (#7604)
* perf(composer): make the single-row autoResize fast path reachable

autoResize() skips the height:'auto' -> scrollHeight -> restore round trip only
when the composer is already at its one-row height, but the guard compared
el.offsetHeight against the CSS min-height (44px) while the composer's natural
one-row border box is line-height + vertical padding + borders (~48px at the
stock font). 48 <= ceil(44)+1 is false, so the skip was unreachable in a real
browser: every append keystroke wrote style.height twice, and the scrollHeight
read that follows forces a synchronous layout of the whole document, transcript
included. The cost therefore grows with the rendered transcript, which is the
"typing gets laggy in long sessions" report.

Accept the natural one-row height as the skip ceiling, computed from the same
already-read getComputedStyle. The strict finite-pixel gate is kept for every
value involved, so a percentage / calc / normal line-height, padding or
min-height still fails closed to the full resize (the #6349 re-gate property is
preserved, and test_issue5514_composer_grow_scroll_pin.py still pins it).

Measured on a 758-message / 5 MB session (31k DOM nodes rendered) with the
browser Event Timing API (input -> next paint) at 6x CPU throttle: a keystroke
went from 168ms to ~136ms median echo latency (72ms with the transcript
detached), 96ms to ~72ms in the windowed state, and the style writes per 8
keystrokes from 16 to 2.

The #5514 invariant holds: an oversized composer still takes the full resize
because its offsetHeight is far above the one-row ceiling.

Tests:
- new tests/test_long_session_composer_typing_latency.py runs the real
  autoResize() body from messages.js in a node sandbox whose getComputedStyle
  returns the composer's computed styles; it is red before this change
  (2 failed, 27 passed) and green after (29 passed).
- tests/test_issue5514_composer_grow_scroll_pin.py source pins updated to the
  new guard shape.

* test(docs): address #7604 review - behavior-only tests + composer sizing docs

Review feedback on the PR flagged two repo-rule items: tests that pin source
strings, and an undocumented runtime behavior change. Both are addressed here.

- tests/test_long_session_composer_typing_latency.py: delete
  test_fix_is_pinned_in_source. It asserted exact JS declarations, which the
  repo testing guidance rejects and which the harness in this module already
  covers by executing the REAL autoResize() body. In its place add
  test_natural_row_skip_survives_every_append_of_a_typed_word, which drives the
  actual typing loop ('' -> 'h' -> ... -> 'hello') and asserts zero height
  writes on every keystroke. Red on the pre-fix tree, green with the fix.
- tests/test_issue5514_composer_grow_scroll_pin.py: convert
  test_single_line_growth_skips_the_height_round_trip from source-string pins to
  a behavior fixture whose getComputedStyle returns the composer's real computed
  styles. It asserts the observable outcome: a one-row append at the natural
  48px row takes no height write, while an oversized 176px composer still
  remeasures back to one row. Red before the fix (2 writes), green after.
- docs/UIUX-GUIDE.md: document the composer sizing invariants in the composer
  section - the single-row skip is a hot path and must stay reachable, its
  ceiling is the natural one-row height or the CSS min-height (whichever is
  larger), everything else still remeasures, and non-px computed values fail
  closed.

./scripts/test.sh on the two touched modules: 29 passed (3 failed on the
pre-fix tree). Neighbour sweep over the 51 composer/scroll/typing modules:
386 passed. ruff check clean.

* fix(tests,docs): derive composer test dimensions from style.css (#7604 review)

Greptile's re-review is right: the fixtures hardcoded the 18px
`data-font-size=large` composer (line-height 29.7px -> a 47.7px one-row box) as
if it were the stylesheet's 16px default (44.4px). At the default the pre-fix
ceiling (ceil(44)+1 = 45px) DID clear the one-row box, so the regression is
font-size dependent - the 16px default reaches the skip, the 18px and 20px
appearance settings do not - and not a stock-configuration bug. The runtime
change is unaffected and unchanged.

- new `tests/_composer_metrics.py`: parses `static/style.css` (the base
  `textarea#msg` rule, the `data-font-size` overrides, the skin overrides) and
  exposes each configuration's box metrics - natural one-row height, the integer
  offsetHeight a browser reports, and both the pre-fix and post-fix ceilings -
  so a fixture cannot encode the wrong font size again.
- `tests/test_long_session_composer_typing_latency.py`: parameterized over every
  configuration the stylesheet can produce, with
  `test_the_regression_was_font_size_dependent_not_stock` pinning the grounding.
- `tests/test_issue5514_composer_grow_scroll_pin.py`: the converted single-row
  test uses the same derived metrics.
- `docs/UIUX-GUIDE.md`: the sizing section now states that the natural row
  tracks the appearance font size (44 / 48 / 51px) while `min-height` stays
  44px, which is why a min-height-only ceiling silently disables the skip.

Pre-fix tree: `4 failed, 31 passed` - exactly the large-18px and xlarge-20px
parameterizations plus the two tests that assert the reported configuration.
Post-fix tree: `35 passed`. Neighbour sweep over the 51 composer/scroll/typing
modules: `392 passed`. ruff clean.
2026-09-16 07:38:43 -07:00
nesquena-hermesandn 03b2be0d38 Release: keep live-follow pinned through above-tail collapse (#7605) (#7606)
Co-authored-by: n <a@n>
2026-09-16 07:07:39 -07:00
Kevin Meinon 906aebefa9 fix(scroll): do not unpin a pinned tail reader on an above-tail collapse clamp (#7605) 2026-09-16 07:03:36 -07:00
nesquena-hermesandn 345ce8baea Release: localize sidebar source-filter tabs (#7602) (#7603)
Co-authored-by: n <a@n>
2026-09-16 02:43:58 -07:00
2ed0c4db27 fix(i18n): route sidebar source-tab labels through t() (#7580) (#7602)
* fix(i18n): route sidebar source labels through t() (#7580)

_sessionSourceLabel() hardcoded the two visible tab labels as English
template literals and assigned the result straight to textContent, so
non-English locales rendered untranslated sidebar tabs. Route both
filters through t() with the established {0}-interpolation pattern
(same shape as kanban_comments_count) and add explicit en + ru keys
so English remains the fallback for locales without a translation.

- static/sessions.js: return t("sessions_source_cli", n) /
  t("sessions_source_webui", n) instead of template literals
- static/i18n.js: add the two keys to the en and ru bundles
- tests/test_issue2351_cli_session_source_filter.py: replace the
  hardcoded-English regressions with locale-key assertions and a new
  interpolation test; verify the template-literal fallback is gone
  so a regression can no longer ro-introduce it silently

* i18n(sessions-source): add source-tab labels to the remaining 13 locales

Codex gate: the runtime fallback is non-regressive, but the repo enforces a
locale key-parity contract — 9 parity tests failed because the two new keys
existed only in EN+RU. Add sessions_source_webui / sessions_source_cli to it,
ja, es, de, zh, zh-Hant, pt, ko, fr, cs, tr, pl, vi, matching each locale's
established translation of 'session' and preserving the {0} count placeholder.
Placed after each locale's _label line so the {0} literal does not fall inside
test_japanese_locale.py's brace-bounded _label regex.

Co-authored-by: Yularzhi <Yularzhi@users.noreply.github.com>

---------

Co-authored-by: nesquena-hermes <agent@hermes.local>
Co-authored-by: Yularzhi <Yularzhi@users.noreply.github.com>
2026-09-16 02:39:58 -07:00
nesquena-hermesandn 5e5b1178af Release: serve SPA for /sessions route (#7598) (#7601)
Co-authored-by: n <a@n>
2026-09-16 00:31:24 -07:00
Skawsk59andSkawsk59 723146cbfe fix: serve SPA for /sessions route (404 after login with auth enabled) (#7598)
Serve the SPA shell for /sessions so the post-login redirect (when password auth is enabled) no longer 404s.

Co-authored-by: Skawsk59 <Skawsk59@users.noreply.github.com>
2026-09-16 00:24:09 -07:00
nesquena-hermesandn 310265c569 Release: fail closed on missing/unreadable Agent Git identity (#7407) (#7599)
Co-authored-by: n <a@n>
2026-09-15 22:18:23 -07:00
leosiedlerandLeonardoLGDS 81f18a1f72 fix(agent_runtime): fail closed when Git identity disappears (#7407)
Fail closed when the Agent Git identity disappears or is unreadable, including when the revision read itself raises — a missing revision is indistinguishable from changed source and must never be treated as proof the loaded code is current.

Co-authored-by: LeonardoLGDS <LeonardoLGDS@users.noreply.github.com>
2026-09-15 22:14:09 -07:00
nesquena-hermesandn d98c9be39e Release: staged prefix reads for oversized metadata fields (#7573) (#7597)
Co-authored-by: n <a@n>
2026-09-15 21:29:34 -07:00
Rodrigo Gomes da Silvaandrodrigogs 66423ad2dc fix(sessions): stop one oversized metadata field from re-parsing the whole sidecar (#7573)
Read the metadata prefix in staged geometric reads so one oversized metadata field no longer forces a full re-parse of the whole sidecar.

Co-authored-by: rodrigogs <rodrigogs@users.noreply.github.com>
2026-09-15 21:25:27 -07:00
Steve Frost d43edcf352 docs(agents): scope documentation pointers by task and state completion criteria 2026-09-15 23:31:40 -04:00
nesquena-hermesandn 8d628f4eae Release: route @custom:<slug> models to their own base_url (#7319) (#7592)
Co-authored-by: n <a@n>
2026-09-15 20:10:06 -07:00
b3nw fb2e8b0088 fix(custom-providers): route @custom:<slug> models to their own base_url (#7319)
Route @custom:<slug> models to their owning custom provider's base_url so a model picked from a second custom provider group resolves correctly instead of 400ing.

Co-authored-by: b3nw <b3nw@users.noreply.github.com>
2026-09-15 20:05:40 -07:00
nesquena-hermesandn 27b7064c4c Release: decode state.db content sentinel in the message projection (#7492) (#7590)
Co-authored-by: n <a@n>
2026-09-15 17:16:53 -07:00
totalitarianandtotalitarian 963e1bae8a Decode state.db content sentinel in the WebUI message projection (#7492)
Decode the state.db content sentinel in the WebUI message projection so structured/multimodal content renders instead of a placeholder, with an out-of-band tuple identity that no scalar can forge and a per-call ContextVar memo for the canonical serialisation.

Co-authored-by: totalitarian <totalitarian@users.noreply.github.com>
2026-09-15 17:12:45 -07:00
nesquena-hermesandn 294577a1d1 Release: native composer field-sizing with fallback parity (#6760) (#7589)
Co-authored-by: n <a@n>
2026-09-15 16:56:11 -07:00
starship-sandstarship-s 2afcdf2676 perf: use native composer sizing where supported (#6760)
Use native CSS field-sizing for composer auto-grow where supported, keeping the JS path as fallback. Includes a fallback-parity fix so an empty composer holds its resting height instead of measuring the placeholder.

Co-authored-by: starship-s <starship-s@users.noreply.github.com>
2026-09-15 16:51:24 -07:00
nesquena-hermesandn 8fa1244279 Release: nest delegated subagent sessions under their messaging parent (#7263) (#7588)
Co-authored-by: n <a@n>
2026-09-15 16:36:51 -07:00
Kenanandlowlandsheperd b56dd423e7 fix(sidebar): nest delegated subagents under messaging parents (#7263)
Nest delegated subagent sessions under the parent conversation that spawned them instead of forcing them to the sidebar top level.

Co-authored-by: lowlandsheperd <lowlandsheperd@users.noreply.github.com>
2026-09-15 16:31:54 -07:00
webtecnicaandwebtecnica 68008e824b fix: classify Signal gateway sessions as messaging (#7572)
Classify Signal gateway sessions as messaging so they group, label, and list like other gateway platforms.

Co-authored-by: webtecnica <webtecnica@gmail.com>
2026-09-15 16:16:57 -07:00
nesquena-hermesandn 572c84005f Release: preserve remote POSIX workspace paths across session, streaming and upload resolvers (#7168) (#7586)
Co-authored-by: n <a@n>
2026-09-15 15:44:39 -07:00
alfred-rootson b71c88f292 fix(workspace): preserve remote POSIX paths across Session & streaming callsites (#7131) (#7168)
Closes the remote POSIX workspace path preservation gap across Session and streaming. Profile-aware runtime path resolution.

Co-authored-by: alfred-rootson <alfred.wojtasordzik@gmail.com>
2026-09-15 15:37:11 -07:00
webtecnica 80610c7b6c docs(architecture): document the /api/models cache invalidation contract
The /api/models cache identity shipped in #7556 (release #7558) changed from a
stat identity to content fingerprints with volatile-key deny-lists, and the #7556
review flagged that this runtime contract had no project-level documentation.
Record the three source-fingerprint axes and their rationale, the one-directional
deny-list invariant, the stat-identity fallbacks (including the deep-tree
RecursionError path), the independent schema/version stamps, and the change
protocol; index the document from docs/CONTRACTS.md. Docs only.
2026-09-13 15:02:03 -03:00
94fd2da830 Release: semantic fingerprint for the Codex catalog dependency (#7556) (#7558)
* fix(config): semantic fingerprint for the Codex catalog dependency (#7540)

* fix(#7556): guard the recursive codex-cache strip against RecursionError

Codex found a valid-but-deeply-nested models_cache.json crashes
_codex_models_cache_fingerprint with RecursionError (the recursive strip ran
outside the fallback try), 500ing /api/models. Move _strip_volatile_codex_cache_fields
inside the existing encode try/except so a transform failure degrades to the
stat-based fingerprint. +1 mutation-proven regression (forces the strip to raise;
fails with RecursionError if the guard is reverted).

* docs(changelog): stamp #7556 (semantic fingerprint for Codex catalog dependency)

---------

Co-authored-by: webtecnica <webtecnica@gmail.com>
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
Co-authored-by: n <a@n>
2026-09-13 17:39:39 +00:00
nesquena-hermesandnesquena-hermes e36f773891 test(#7510): clean up ruff F401/F841 in the raw-pending-approval test (#7552)
Follow-up to #7551: the merged test file carried an unused `route_approvals as ra`
import and two unused `entry =` assignments (advisory lint gate was red, though not
a required check). Drop them; the two `entry` bindings that are asserted on are kept.

Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-09-13 03:16:48 +00:00
e2ce040f1f Release: actionable ids for raw id-less pending approvals (#7510) (#7551)
* fix(approvals): mint actionable ids for raw id-less local pending entries

A raw id-less approval entry in _pending (agent-side _pending_result drops
{command, pattern_key, pattern_keys, description} verbatim when no gateway
notifier is registered for the session) is unactionable by contract: the
frontend owner-capture requires an approval_id, so the approval card can
neither be approved nor dismissed - the 1.5s poll re-serves it forever.

reconcile_gateway_pending_mirror_locked already mints gwlocal:<token> ids
for id-less _gateway_queues producers; extend the same contract to raw
non-mirror _pending entries with a stable gwlocal-mirrorless:<uuid> id
minted once onto the stored entry dict, so the id persists across polls
and frontend dismiss markers stick.

The exact-id respond path needs no further change: once the entry carries
an id, _resolve_approval_legacy pops it (also clearing the stale
stream-pointer 409 guard), stale explicit ids still fail closed (#527),
and the yolo Skip-all release drains it.

* docs(changelog): stamp #7510 (actionable ids for raw id-less pending approvals)

---------

Co-authored-by: CharlesMcquade <6466275+CharlesMcquade@users.noreply.github.com>
Co-authored-by: n <a@n>
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-09-13 03:07:49 +00:00
e4d9b799e4 Release: manual title regeneration for queued-user transcripts (#7545) (#7547)
* fix(api): scan to first complete exchange for title regeneration (#7543)

_first_exchange_snippets() aborted at a second consecutive user message
before collecting assistant text, so transcripts opening with queued user
rows (channel-backed imports) hit the missing_exchange rejection and the
deterministic fallback got persisted with HTTP 200 — invisible in the UI
and unrecoverable by retrying.

Keep scanning until the first complete user+assistant pair instead, and
fall back to the last complete exchange when the opening user message
yields no user text (privacy scrubbing) instead of failing with
empty_user_message.

Tests: tests/test_issue7543_title_first_exchange.py (8 cases, incl. the
issue-pinned transcript shape and walker-mocked discriminator tests for
both fixes).

* test(api): drop local-machine notes and sys.path hack from 7543 test module

- Docstring described the change as a local-only uncommitted hotfix with
  machine-specific paths — wrong for an upstream PR and unsafe to publish.
- Drop the defensive REPO_ROOT sys.path insert; the repo conftest already
  puts the package root on sys.path (matches the neighboring test modules).

* fix(api): drop unreachable latest-exchange fallback from title regeneration

Greptile flagged the mocked-walker fallback test (P2) as fabricating
internal state, and an exhaustive enumeration of realistic transcript
shapes (5460 combinations, lengths 1-6) confirms it: both walkers share
the same text extraction and the forward walker scans every message, so
'first walker empty while latest walker yields a pair' is unreachable
from real transcripts. The fallback branch was dead code.

Remove it together with its two walker-mocked tests; the fixed first-
exchange walker alone resolves the reported issue shape. Replace the
mocked tests with an observable E2E error-path test through the real
walkers, and make the aux mock guard-faithful (empty assistant_text ->
missing_exchange per the generate_title_raw_via_aux contract) so the
fail-before proof actually exercises the endpoint contract.

* fix(#7543): scope first-exchange scan-past to the manual-regen path only

The full suite caught that broadening the SHARED _first_exchange_snippets also
activated the automatic in-stream background-title path for consecutive-user-row
transcripts, moving stream teardown from the synchronous stream_end branch to the
background-title-thread branch (test_issue3929 emits_done regressed: stream_end
was no longer in the synchronous queue). #7543 is specifically about the MANUAL
'Regenerate title' endpoint, so gate the scan-past behavior behind an opt-in
scan_past_consecutive_users flag (default False = master behavior) and pass True
only from generate_session_title_for_session. Auto in-stream path is now
byte-identical to master; added a default-behavior regression guard test.

* docs(changelog): stamp #7545 (manual title regen for queued-user transcripts)

---------

Co-authored-by: Raylands <Raylands@users.noreply.github.com>
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
Co-authored-by: n <a@n>
2026-09-12 21:12:52 +00:00
b1286878a4 Release: models_discovered/discover_models live-catalog fix (#7409) (#7538)
* Apply reviewed change

* fix(models): honor discover_models opt-out + preserve discovered catalog on live-probe failure

Codex gate found two CORE edge cases in the #7404 fix:
1. discover_models:false must override models_discovered:true (explicit opt-out
   pins the configured allowlist; honor Agent string forms false/no/0).
2. a transient empty live-catalog probe dropped a discovered provider's models
   (cached empty ~24h); now falls back to configured discovered IDs merged with
   any static catalog (deduped, ordered first).

Mirrors the Hermes Agent canonical semantics (model_switch_providers._discover_flag,
model_switch._models_config_is_allowlist). Adds 3 mutation-proven regressions.

* docs(changelog): stamp #7409 (models_discovered/discover_models live catalog)

---------

Co-authored-by: evgenyponomarev <712802+evgenyponomarev@users.noreply.github.com>
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
Co-authored-by: n <a@n>
2026-09-12 05:32:16 +00:00
nesquena-hermes 52bcfa3cb9 Merge pull request #7537 from nesquena/stage-relmgr-0912c
exp-v0.52.299 — graceful SIGINT shutdown (#7315)
2026-09-11 20:28:03 -07:00
nesquena-hermes 529488057e docs(changelog): stamp #7315 (SIGINT graceful shutdown) 2026-09-12 03:22:07 +00:00
nesquena-hermes c10bbd8e33 stage-fix(#7315): keep explicit signal.SIGTERM literal + add SIGINT within 749-line budget
Two tests pin opposite constraints on server.py: test_sprint10 requires <750 lines,
and test_issue5543 AST-asserts a signal.signal() call whose first arg literal is
signal.SIGTERM. The loop refactor satisfied the size budget but the loop var _sig
failed the AST literal check. Register both signals explicitly in one shared
try/except: keeps the SIGTERM literal, adds SIGINT (#7078), stays at 749 lines.
2026-09-12 03:03:10 +00:00
n c8a443454c Stage: PR #7315 — SIGTERM+SIGINT graceful shutdown by @aniruddhaadak80 (closes #7078) 2026-09-12 02:44:44 +00:00
nesquena-hermes a8f3933fd0 fix(server): register SIGTERM+SIGINT via one loop to stay under the 749-line budget
The contributor's SIGINT addition (#7315) pushed server.py to 754 lines, tripping
test_sprint10::test_server_py_under_750_lines (master was 749). Fold the two
signal.signal() try/except blocks into a single loop over (SIGTERM, SIGINT) so
SIGINT graceful shutdown lands (#7078) without growing the file past the enforced
split budget. Same _request_shutdown handler, same fail-safe on non-main-thread.
2026-09-12 02:29:28 +00:00
nesquena-hermes 00afa1eb31 Merge pull request #7536 from nesquena/stage-relmgr-0912a
exp-v0.52.298 — static MIME types (#7372) + upload TOCTOU (#7337)
2026-09-11 18:49:42 -07:00