38 Commits
Author SHA1 Message Date
carlotestor 075f95aaf2 fix(console): retry a late profile-mismatch 409 once when focus returned mid-request 2026-09-24 17:09:08 +00:00
carlotestor cfe13b5fc2 fix(gateway): send Stop and approval replies to the Gateway that owns a reattached run
stop_gateway_run() and the /api/approval/respond relay resolved the process-active profile's Gateway, while a reattached run is polled on its session profile's Gateway. Record the endpoint per stream when the worker resolves it and look it up by run id; fall back to the process Gateway only when no live stream holds the run.
2026-09-24 16:56:58 +00:00
carlotestor 2dd1366033 fix: fingerprint .env values, parse manifests like the agent, gate stale models fallback on sources 2026-09-24 15:29:51 +00:00
carlotestor 00973abe0c fix(gateway): reattach with the session profile's own endpoint (root too); keep the prompt when a replayed run ends cancelled 2026-09-24 15:18:53 +00:00
carlotestor 6ebd12c251 fix(console): re-arm profile-paused approval/clarify pollers on focus/visibility 2026-09-24 14:55:27 +00:00
carlotestor 96bdeeb9cf Merge remote-tracking branch 'origin/master' into HEAD 2026-09-24 14:54:20 +00:00
carlotestor 05aae134cf Merge branch 'master' into feat/gateway-run-reattach-after-restart 2026-09-24 15:51:42 +01:00
carlotestor 5fdc881799 fix(gateway): do not admit a run whose recovery checkpoint failed to save 2026-09-23 22:05:09 +00:00
carlotestor b9fb2f7065 fix(gateway): reattach under the session profile and recover runs admitted before the id was saved 2026-09-23 21:56:14 +00:00
carlotestor d6bf9900c6 docs: document /api/git-info state.db fallback and poller 409 stop 2026-09-23 19:28:24 +00:00
carlotestor 5d590bdc7f refactor: shrink gateway reattach diff without behaviour change
Inline endpoint resolution, fold HTTP error branches, merge duplicate
tests into parametrized cases, trim comments and docstrings.
2026-09-23 19:12:46 +00:00
carlotestor fbb0843acf fix(console): stale profile-mismatch response must not stop the replacement poller
Each approval/clarify fallback poller captures its own interval handle; a 409
(or finally) from a request issued by a replaced poller no longer stops or
clears the in-flight guard of the poller that superseded it.
2026-09-23 19:11:25 +00:00
carlotestor 7344c8df77 refactor: shrink profile-switch models cache diff
Reuse _get_models_cache_path(profile) for profile delete/create instead of a
second path helper, derive the fingerprint home from the auth store path, fold
the three switch test files into one parametrized file, and trim comments/docs.
Behaviour, source axes and switch fast path are unchanged.
2026-09-23 19:11:19 +00:00
carlotestor c713c6efc2 fix(console): stop git-info 404s, profile-mismatch 409 poll loops and the wasted pwa-startup preload
- /api/git-info falls back to state.db metadata for sessions without a WebUI
  sidecar (CLI, delegated subagents), mirroring /api/list, instead of 404ing.
- Approval and clarify fallback pollers stop on session_profile_mismatch 409
  instead of re-requesting every 1.5s/3s; live cards stay standing.
- Drop <link rel=preload> for pwa-startup.js: the preload scanner resolves it
  before the dynamic <base href> is written, so on /session/<id> it fetches
  /session/static/... while the script loads /static/..., a wasted duplicate
  request plus a 'preloaded but not used' warning on every load.
2026-09-23 18:29:11 +00:00
carlotestor 35bb952def fix: fingerprint model-provider plugin file contents, not just manifest version 2026-09-23 18:00:35 +00:00
carlotestor 94b07588dd fix(gateway): resolve reattach URL and key as one pair; fail fast on 401/403
Drop the persisted base_url: pairing a saved URL with the profile's current key could poll the
old gateway with a new credential. Both now come from the session profile's current config, and
an auth rejection settles the turn at once with an explicit error instead of 150 retries.
2026-09-23 17:08:31 +00:00
carlotestor 65e336fb16 fix(gateway): reattach through the session's profile and scan sidecars, not the index
- resolve the reattach gateway URL/key inside profile_scope_for_detached_worker(session.profile)
  and prefer the base_url persisted when the run was accepted
- discover pending turns from the sidecars (head read skips idle files) instead of the derived
  _index.json, which can be missing, corrupt, or lag a save
2026-09-23 17:00:38 +00:00
carlotestor 2a742e13d6 Merge remote-tracking branch 'origin/master' into HEAD 2026-09-23 16:34:11 +00:00
carlotestor 4632ab65e9 Merge remote-tracking branch 'origin/master' into HEAD 2026-09-23 16:34:03 +00:00
carlotestor 7ba246bfc0 fix(gateway): reattach runs-API turns after a WebUI restart instead of marking them interrupted
A gateway-backed turn on the runs API executes in the Hermes Gateway, but the
run id only lived in process memory, so a WebUI restart orphaned the turn and
stale-pending repair appended 'Response interrupted' while the run finished
on the gateway.

- persist session.gateway_run {run_id, stream_id} as soon as POST /v1/runs returns
- send Idempotency-Key webui-<stream_id> so the gateway keeps a durable run record
- on startup (after bind, before serving) re-register the stream and follow
  GET /v1/runs/{id} until terminal; write back the real output, surface a
  parked approval once, keep Stop working, clear the record on every settlement
2026-09-23 16:30:21 +00:00
carlotestor 4e17333516 fix: fingerprint .env key names with the provider .env loader 2026-09-23 15:52:14 +00:00
carlotestor 7f2e812280 fix: fingerprint .env key names and provider plugins; drop a profile's models cache on delete/create
A retained per-profile snapshot is now rejected after a switch when the target
profile's .env credential set or model-provider plugins changed, and a
recreated same-name profile no longer inherits the deleted profile's catalog.
2026-09-23 15:31:44 +00:00
carlotestor 35921eccbb fix(sidebar): scope project inheritance to subagents, resolve lineages once
Forks and other child sessions keep their own project_id, so a fork moved to
No project stays Unassigned. A per-render memoized, cycle-safe resolver is
shared by partitioning, reference scoping and the Unassigned chip.
2026-09-23 15:28:47 +00:00
carlotestor 35c9b8ae96 fix(sidebar): Unassigned chip resolves child projects through reference rows
The chip check now reuses the ancestor map built by _partitionSidebarSessionRows,
so a child whose project parent is only a hidden reference row is not counted
as unassigned. Document project inheritance for subagent sessions in README.
2026-09-23 12:59:36 +00:00
carlotestor d04737ec87 fix(sidebar): show delegated subagents inside their parent's project
Subagent child sessions come from state.db and never carry a project_id,
so the project filter dropped them. Resolve a child's project from its
nearest ancestor for the project filter, reference scoping and the
Unassigned chip.
2026-09-23 09:25:33 +00:00
carlotestor 5c24458dff fix(sidebar): derive the route session cap from the shared limit
The env override never reached /api/sessions because the route kept a
separate hard-coded cap of 20. Resolve the window in api.config before
profile init, clamp it at 200, and refuse a profile .env override.
2026-09-22 17:50:56 +00:00
carlotestor 78bb366560 fix: keep the target profile's models disk cache on the first /api/models after a per-client switch
switch_profile(process_wide=False) skips reload_config(), so _cfg_mtime still
held the previous profile's nonzero mtime. The first /api/models for the new
profile took the config-reload branch and _refresh_config_cache() treated
_old_cfg_mtime != 0.0 as a config edit, unlinking the *target* profile's
models_cache.<name>.json - defeating delete_disk=False on the very next request.

Only delete the disk snapshot when the same config.yaml path was already loaded
(_old_cfg_path == config_path); a path change (profile switch) keeps it. Executed
two-profile end-to-end test (real homes, configs, cache files) plus a guard that
a same-profile config edit still deletes the snapshot. Doc table updated.
2026-09-20 11:19:41 +00:00
carlotestor 899b89e1d3 docs+test: document HERMES_WEBUI_VISIBLE_SESSION_LIMIT; probe it in a subprocess
- README env table + .env.example gain the new setting (default 20, fallback
  semantics), pinned by test_setting_is_documented.
- tests/test_visible_session_limit_env.py no longer importlib.reload()s
  api.models in the shared pytest process (which recreated its module-level
  locks/caches/classes behind other modules' backs); each case now imports
  api.models in a fresh interpreter with a scrubbed env, matching
  test_issue3283_profiles_config_import_order.
2026-09-20 11:19:41 +00:00
carlotestor 093191cfb8 docs: state memory and disk behaviour per invalidation path (provider invalidation and config reload also delete the disk snapshot) 2026-09-20 11:19:41 +00:00
carlotestor 63d024dc45 feat(sidebar): make the visible-session window size configurable
CLI_VISIBLE_SESSION_LIMIT is hardcoded to 20. That constant also bounds
how many delegated subagent children can render at once, since a child
only nests when its parent row is in the same payload, so operators
running wide delegate_task fan-outs cannot see all children without
editing the source.

Read it from HERMES_WEBUI_VISIBLE_SESSION_LIMIT via the existing
_env_int helper, keeping 20 as the default.
2026-09-20 11:19:41 +00:00
carlotestor 0415d07cd0 docs: invalidate_models_cache is the only delete_disk entry point, not the only snapshot-dropping path 2026-09-20 11:19:41 +00:00
carlotestor 217a2fcea8 review: document delete_disk invalidation modes; assert switch route kwargs via executed handle_post
- docs/architecture/models-cache-invalidation.md: add 'Invalidation modes'
  section describing delete_disk=True/False and why /api/profile/switch
  uses the memory-only mode (fingerprint-guarded per-profile disk cache).
- tests: replace the source-text scan of the switch route with two executed
  routes.handle_post tests: one captures invalidate_models_cache kwargs and
  asserts exactly [((), {'delete_disk': False})]; one runs the real
  invalidate_models_cache and asserts the disk file survives while the
  in-memory catalog is dropped. Both fail if the route reverts to the
  default delete_disk=True.
2026-09-20 11:19:41 +00:00
carlotestor 7bb12d0aab fix: keep per-profile models disk cache across /api/profile/switch
POST /api/profile/switch called invalidate_models_cache(), whose
test-isolation contract also unlinks models_cache.<profile>.json. On a
switch nothing about the sources changed, so the delete only forced a
full cold rebuild (live provider fetch_models HTTPS calls, ~490k deepcopy
calls) on the very next /api/models — several seconds on every switch.

The disk cache is already keyed per profile and rejected on read when
_models_cache_source_fingerprint() differs, so it is safe to keep. Add a
delete_disk keyword (default True — every existing caller keeps the old
behaviour) and have the switch route pass delete_disk=False.

Regression tests cover: memory-only drop preserves the disk file, default
call still unlinks it, the next get_available_models() serves the disk
snapshot without a live rebuild, and the switch route opts out. Two
existing zero-arg stubs of invalidate_models_cache accept **kwargs now.
2026-09-20 11:19:41 +00:00
carlotestor 46e60616f6 fix(projects): tag scan-minted system projects to the scanned profile
The all-profiles sidebar scan walks every profile's state.db in one
request. When it lazily minted a "Webhooks" / "Cron Jobs" project for a
profile that had webhook/cron rows, ensure_webhook_project() and
ensure_cron_project() tagged the new entry with get_active_profile_name()
-- the UI-selected profile -- instead of the profile whose rows triggered
it. A webhook-only profile therefore leaked a "Webhooks" project onto
whichever profile was selected; deleting it did nothing because the next
sidebar poll re-minted it, and switching profiles minted one per profile.

ensure_webhook_project / ensure_cron_project / _profile_has_user_projects
now take an explicit profile (default: active profile), and the scan
passes _cli_profile through, so the opt-in gate and the tag both key on
the scanned profile. Existing stubs in tests updated to accept the kwargs.
2026-09-18 09:13:09 +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
656575a400 Fix geometric duplication of recovered reasoning-only rows at turn settle (refs #7388, independent of #7416) (#7443)
* Fix geometric duplication of recovered reasoning-only rows at turn settle

_restore_display_reasoning_metadata() re-inserted every display-only
reasoning row at its API-safe position in the new result. After context
compaction that position is past the end of result_messages, so the row
was appended after the new reply; _message_identity() is None for an
empty assistant row, so the display merge could not dedup it. Each
settled turn therefore re-added every existing empty
_recovered_from_run_journal anchor once more (1,1,2,4,8,... blocks).

Only restore a reasoning-only row in front of its own API-safe
successor; a compacted result with no aligned slot is skipped.

Refs #7388, #7416 (independent: that PR fixes linear journal replay).

* tests: keep docstrings within the 5-line block budget

* Prefer stable ids when aligning a reasoning row's successor at restore

The successor-alignment guard compared the historical successor with the
aligned current row via _message_identity(), which ignores stable ids and
returns equal keys for repeated prompts ("continue"). A distinct current
turn with the same text false-matched the historical successor, every
recovered anchor before it was re-inserted, and 64 anchors became 128
after one restore->merge settle (nesquena-hermes gate on ed2076eda).

Alignment is now stable-id-first: when either row carries an id, both
must carry the same id; _message_identity() is the fallback only when
neither row has one. Adds a production-order regression through
_settle_result_messages() with 64 anchors, stable ids and repeated
"continue" prompts (RED on ed2076eda and origin/master: 128 != 64), plus
a unit test pinning all four id-presence branches.

* Fail closed on stable-id identity: positive-int only, unique, no content fallback

_restore_display_reasoning_metadata() compared ids with raw equality, so a
persisted historical successor with id=True (or 1.0) matched a distinct
current row minted as id=1 and restored the reasoning card before the
wrong turn. Ids are now accepted only when type(value) is int and > 0,
both rows must carry a valid equal id whenever either has an id field,
and the id must be the sole owner in both API-safe projections.
_assign_stable_message_ids() re-mints invalid ids so they cannot survive
into a collision with minted integers.

* tests: explicit zip strict= for the ruff B905 gate

* Resolve the active-turn boundary before restoration; never carry historical ids/rows into it

A historical assistant successor with the same content as the distinct
current assistant ("Done." / "Done.") let _restore_reasoning_metadata copy
the historical stable id onto the current row; the uniqueness guard then
accepted that forged identity and 64 recovered cards settled to 128.

_active_turn_boundary() is decided once in _prepare_marker_clean_writeback
before any restore (token > strict context prefix > last matching user row
> 0) and threaded through _settle_result_messages into both restores.
Rows at/after it receive no historical id/metadata and no historical
reasoning-only row. Assistant-only results are boundary zero.

* Keep the pinned _restore_reasoning_metadata signature; boundary goes through _restore_reasoning_metadata_before_boundary

tests/test_sprint49.py pins the two-arg signature as source text, so the
boundary-aware core lives in _restore_reasoning_metadata_before_boundary
and the original name stays a thin no-boundary wrapper.

* Content-only prefix is not turn ownership: fail closed to boundary 0; sync route passes result/Agent turn authority

- _active_turn_boundary: a previous-context prefix matched by _message_identity
  (ids/timestamps ignored) only narrows where the current user may be; the
  boundary is the prompt-matching user AT OR AFTER that prefix, else 0.
- _handle_chat_sync: resolve authority via _resolve_active_turn_authority
  (result, agent) instead of hard-coding None.
- Regressions: three-row current-only collision (boundary 0, one 64 block),
  exact-authority + append-only controls, ID-less-successor settle/save/load
  pinned at the persisted-display boundary, sync-route authority pass-through.

---------

Co-authored-by: carlotestor <carlotestor@users.noreply.github.com>
Co-authored-by: nesquena-hermes <nesquena+hermes@gmail.com>
2026-09-10 12:29:33 -07:00
carlotestor 7d2ca8cabb fix: address greptile review on SessionDB adopt helper
- Log failed closes in _adopt_session_db_for_cached_agent with
  logger.debug(..., exc_info=True) instead of bare pass, matching
  _replace_session_db_in_kwargs so a failed close (EMFILE risk) is
  diagnosable.
- Drop the fragile exec/source-slice fallback in the test helper import;
  real import or pytest.skip. Source-level pins still cover reverts
  without importing streaming.
2026-07-16 16:53:42 +02:00
carlotestor 0cee7dba47 fix: stop closing SessionDB under live background subagents
Background subagents (delegate_tool) capture parent_agent._session_db by
reference. The cached-agent reuse path created a fresh SessionDB every
stream request and unconditionally closed the previous handle (PR #1421
EMFILE fix). A server-side wakeup / new turn for the same parent session
could therefore close the shared handle while children were still
flushing messages, producing:

  Session DB append_message failed: 'NoneType' object has no attribute 'execute'

Reproduce (observed in production):
1. Parent WebUI session runs delegate_task with N background subagents
2. Parent turn tears down / idle-hook redelivers a deferred wakeup
3. New stream for the parent adopts a fresh SessionDB and closes the old one
4. Live subagents' append_message hits conn=None

Fix: _adopt_session_db_for_cached_agent reuses a still-open handle (and
closes the unused new one — still EMFILE-safe) and only replaces when
the existing handle is missing or already closed. Same guard applied to
_replace_session_db_in_kwargs (credential self-heal). LRU eviction still
closes at true session boundaries.

Tests updated to pin the adopt helper and the subagent-safe semantics.
2026-07-16 16:38:43 +02:00