* docs: weekly contributor tally refresh (20260619)
Regenerated by scripts/regen_contributors.py --apply. Includes the source (e)/(f)
ship-signal accuracy fix that recovers systematically-undercounted cherry-picked
contributions (~77 contributors). 280 contributors / 1559 credits (was 263/1232).
Markers spliced, prose untouched, --verify clean, 0 double-counts.
* docs: align CONTRIBUTORS header prose with the 5-source credit algorithm
Addresses greptile P2: the header still described the old three-source union
(merged-PR list + CHANGELOG + Co-authored-by) but regen_contributors.py now adds
two more — ship-comment scans and absorb-commit scans — for five total. Updates
the prose to match what the script actually runs.
---------
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
* Release PQ (v0.51.456): start.sh detects an already-running server (#4308)
start.sh now probes /health (best-effort, 2s, curl/wget) before launching
bootstrap.py; if a server already answers it prints a clear 'already running +
how to restart' message and exits 0 instead of spawning a child that fails to
bind. Brings the detached start.sh path to parity with ctl.sh's double-start
refusal. Container path (server.py direct) unaffected.
Co-authored-by: tomas-forgac
* gate fix (Codex CORE): parse $@ for --host/--host=/positional port in the start.sh preflight
bootstrap.py accepts a positional port + --host that override the env defaults;
the preflight probed env/defaults only, so './start.sh <altport>' while a server
ran on the env-default port falsely reported 'already running' and refused the
legitimate launch. Now scans $@ the same way bootstrap.py's argparse does before
building the /health probe URL.
---------
Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
_drain_webui_process_notifications now drops (consumes, never requeues) a
completion whose completed_at is older than a configurable cap
(HERMES_WEBUI_STALE_COMPLETION_MAX_AGE_SECONDS, default 6h; 0 disables).
Fail-open: events without completed_at (legacy agent) are never dropped.
Additive (+192/-0). Closes#4029.
Co-authored-by: allenliang2022
Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
start.sh exec's bootstrap.py without --foreground, which double-forks into a
detached daemon, so it belongs in the 'find PID + kill' row, not the Ctrl-C
foreground row. Docs-only, no version stamp.
Co-authored-by: tomas-forgac
Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
Translated 69 remaining English fallbacks in the ja locale block to parity
with English; proper nouns/abbreviations intentionally left as-is. Values only,
no key changes (key parity verified). 265 locale/i18n tests pass.
Co-authored-by: koshikai
Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
Approve/deny on a remote-gateway backend now forwards to the gateway runs API
(run_id server-derived, existing auth+CSRF preserved, capability-probed + 60s
cached). Non-gateway/local approval path unchanged — relay only fires when a
session has an active gateway run (_STREAM_RUN_IDS). Old gateways get a clear
upgrade message instead of a silent no-op.
Co-authored-by: rodboev
Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
* Release PL (v0.51.451): show named-profile external sessions in the all-profiles list (#4067, #4065)
All-profiles session enumeration now scans across profiles only when the
request explicitly asks (all-profiles view); cross-profile import resolves
to the active profile's state.db for unqualified requests (foreign id -> 404),
mirroring the #3982/#3991 detail/export profile-scoping gate.
Co-authored-by: rodboev
* gate fix (Codex CORE + Opus): close cross-profile leak on the import_cli refresh/existing-session branch
The already-imported branch of _handle_session_import_cli forced allow_all_profiles=True
whenever the globally-stored session carried a profile, with no active-profile visibility
gate — so an unqualified request from profile A could read/refresh a session imported under
profile B's live state.db (both advisors reproduced the foreign-transcript leak). Now gated
exactly like the /api/session detail + export endpoints: unqualified requests require
_session_visible_to_active_profile; explicit all_profiles requires a matching profile.
+ 3 regression tests.
* gate fix (Codex SILENT, re-gate): thread source_filter into the all-profiles CLI scan (models.py:4113)
The all-profiles branch keys its cache on source_filter but called
_load_cli_sessions_uncached without it, so /api/sessions?all_profiles=1 with a
source filter returned every-source sessions under a filtered key. + regression test.
---------
Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
* Release PK (v0.51.450): preserve custom provider namespace prefixes in model normalization (#4278)
Token-boundary check in _normalize_provider_id (exact token or prefix
followed strictly by ':' or '/'); explicit aliases for hyphenated
first-party names (google-gemini, google-ai-studio, claude-code); bare-prefix
family detection no longer fires on namespaced model IDs.
Closes#4278. Supersedes #4280 (rodboev) whose '-' boundary re-collapsed
claude-relay->anthropic.
Co-authored-by: b3nw
* gate fix (Codex CORE): add openai-api to _PROVIDER_ALIASES so the :/ token boundary doesn't drop the registered openai-api provider id to '' (would silently repair OpenAI API sessions to the profile default) + 2 regression tests
* gate fix (Opus): gate the streaming send-path bare-prefix loop on '/' not in model so custom namespaces (gemini_cli/, gpt_proxy/, claude-relay/) survive the send path — same #4278 collision Opus found in _apply_profile_provider_context_to_streaming_model; + regression test + CHANGELOG
---------
Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
* test-infra: per-process free port + port-keyed state dir (true parallel safety)
ROOT CAUSE of the cascade-under-concurrency: the test port was a fixed
md5(repo_path)%10000, so two concurrent pytest runs from the SAME worktree (e.g.
a dev's suite + a Codex/Opus gate running ./scripts/test.sh to verify) bound the
SAME port — and the session-fixture's _kill_port_owner(TEST_PORT) at setup reaped
the OTHER run's server mid-suite, cascading every HTTP test with ConnectionRefused.
FIX: when HERMES_WEBUI_TEST_PORT isn't pinned, _auto_test_port now grabs a FREE
OS-assigned ephemeral port per process (bind :0, read back, release). The state
dir name is keyed on that port too (webui-test-<repohash>-<port>), so concurrent
runs get fully distinct port + state dir + db — no shared resource, no mutual kill.
VERIFIED: two FULL suites run concurrently from the same worktree → BOTH
9150 passed / 0 failed, 0 ConnectionRefused, 0 dirs leaked into ~/.hermes. This is
the parallel-safe world: Codex + Opus gates + local suite can all run at once.
* test-infra: address Codex CORE findings on the per-process-port fix
1. Don't _kill_port_owner an AUTO (unpinned) port: fuser -k on an ephemeral-range
port can match a client socket or a concurrent run's process, killing the wrong
thing. Track TEST_PORT_PINNED; only reap pinned ports (which may be stale).
Auto ports are freshly confirmed-free, so nothing to reap.
2. Cap auto port at <=64535 so offset-deriving tests (test_server_port_exclusivity
adds up to +905 to TEST_PORT) can't overflow past 65535 (OverflowError).
---------
Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
* test-infra: hard-isolate test state from production + robust teardown
Two production-safety fixes to tests/conftest.py + tests/_pytest_port.py:
1. TEST_STATE_DIR now defaults under the OS temp dir (/tmp/hermes-webui-tests/)
instead of HERMES_HOME/webui-test-<hash> — which had been writing test state
INTO the real ~/.hermes/profiles/webui/ (144 leaked webui-test-* dirs found).
Added a hard-stop guard: refuse to run if the resolved test state dir is
inside the production Hermes home, so a misconfigured env can never let tests
wipe/clobber production profiles/sessions/credentials.
2. _rmtree_retry is now resilient on Linux (was a bare shutil.rmtree): retries
transient OSError [Errno 39] Directory-not-empty races (a background daemon
writing into the tree during teardown) + a final ignore_errors sweep, and
warns instead of raising so a benign cleanup race can't spuriously ERROR a
test (the failure mode seen near #4283 context-reconciliation tests).
* test-infra: anchor production-proximity guard on literal ~/.hermes (not $HERMES_HOME)
The first guard compared TEST_STATE_DIR against $HERMES_HOME, which is overridden
to TEST_STATE_DIR itself during a run -> false-tripped collection (6 modules) with
'test dir X is inside production home X'. Now anchor on the literal user-home
~/.hermes and explicitly allow any dir under the OS temp root, so the temp default
never trips while a real ~/.hermes target still hard-stops.
* test-infra: close TMPDIR-under-~/.hermes guard hole (Codex)
Codex flagged: the 'temp is always allowed' exception could be exploited if
TMPDIR itself is set under ~/.hermes — then a 'temp' path is a production path and
_under_temp would wrongly suppress the guard. Now the temp exception only holds
when the temp root is itself OUTSIDE the production home. Verified: normal /tmp
default allowed; TMPDIR=~/.hermes/tmp now trips the guard.
* test-infra: mirror production-proximity guard into _pytest_port.py (Opus defense-in-depth)
* test-infra: remove ~/.hermes production-path fallbacks from 4 test files (Codex)
Codex's kickback found the real gap: test_sprint5, test_sprint45,
test_onboarding_existing_config, test_regressions each hardcoded a
~/.hermes/webui-mvp-test fallback for their state dir (used when running
standalone or if the env var is unset). Repointed all to the shared isolated
TEST_STATE_DIR from _pytest_port (temp-rooted). Also refreshed stale
~/.hermes/webui-mvp-test references in test_gateway_sync comment + ARCHITECTURE.md.
Now ZERO production-path fallbacks remain in tests.
---------
Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
test_profile_switch_restarts_watcher_best_effort and
test_profile_switch_response_survives_watcher_restart_failure passed in isolation
but failed in the full suite (HTTP 409 != 200) when an auth-enabling sibling test
left is_auth_enabled() True in-process: /api/profile/switch -> build_profile_cookie
-> sign_profile_cookie_value raises (no session) -> RuntimeError -> 409. Added an
autouse fixture forcing api.auth.is_auth_enabled -> False for this module (these
tests exercise watcher restart, not auth). Verified: full suite 9139 passed, 0
failed. Test-only.
Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
* stage-3712: model picker show-all overflow + fix per-row count leak (#3691)
Self-rebased rodboev's #3712 onto master. FIX (Nathan-caught): the per-row
provider chip stripped the '(N of M)' overflow count — that count belongs on the
group heading only, and was reading as nonsense post-expand (row 30 showing
'15 of 30'). Updated test to assert heading-carries-count + rows-do-not.
Co-authored-by: rodboev <rodboev@users.noreply.github.com>
* stage-3712: + dedupe overflow expand (Codex SILENT) — reuse existing injected option
Codex caught: select hidden overflow model from search → reopen → Show all could
duplicate the row (_appendOverflowOptionsToGroup blindly appended every extra
model without checking the select for an already-injected option). Now moves/reuses
an existing same-value option instead of re-creating it.
* stage-3712: + fix expanded-heading double-count (Opus) — strip decoration, count rendered rows
Opus caught: post-expand the heading showed 'Nous (2 of 4) (4)' (appended its own
count onto the already-decorated backend label) and used modelCount (incl hoisted-
configured) vs rendered rows. Now strips the '(N of M)' suffix when expanded +
counts groupRows.length. Test asserts no double count on out[expanded].
* stage-3712: + include extra_models in Settings/Cron/Profile/Aux native selectors (Codex CORE)
Codex caught: the server overflow split moves models>15 into extra_models for ALL
providers, but the 4 native <select> builders in panels.js (settingsModel:6690,
cronFormModel:1112, profileFormModel:5784, _auxProviders:8287) read g.models only
— silently dropping the overflow tail for large providers. Now concat
models+extra_models at all 4 sites. Test guard added.
* Release PG: model picker show-all overflow for large provider catalogs (#3691)
Self-rebased rodboev's #3712 onto v0.51.445; full browser-tested (desktop+mobile,
before/after) + Nathan screenshot-approved. 4 gate-caught issues fixed inline:
per-row count leak (Nathan), overflow-expand dedup (Codex), heading double-count
(Opus), and native Settings/Cron/Profile/Aux selectors dropping the overflow tail
(Codex CORE). Codex SAFE + Opus SHIP + 41 picker tests green.
Co-authored-by: rodboev <rodboev@users.noreply.github.com>
---------
Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
Co-authored-by: rodboev <rodboev@users.noreply.github.com>
rodboev test-only #4274 (test_issue609, test_sprint3, + 2 others). Linux
assertions stay at full strictness. 63 tests pass. No version stamp (tests-only).
Note: #4273 was NOT included — it touches api/workspace.py runtime (home-expansion
in the blocked-roots boundary), needs a separate full gate.
Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
Co-authored-by: rodboev <rodboev@users.noreply.github.com>
* stage-3629: scope gateway watcher to active profile (profile-keyed registry), rebased onto master
* stage-3629: + stop-race-safe subscribe() (Codex CORE) + regression test
Codex caught a TOCTOU between get_watcher() and subscribe(): a watcher reaped as
subscriber-free during a concurrent profile switch left a late subscriber's queue
without the None sentinel -> SSE loop hangs open (keepalives, no events). Opus
independently found the same race (rated follow-up). subscribe() now checks
_stop_event under _sub_lock after appending and self-enqueues the sentinel.
test_subscribe_after_stop_gets_sentinel_immediately pins it.
* stage-3629: + reconnect gateway SSE to new profile on switch (Codex CORE #2)
Second Codex finding: after a profile switch this tab's _gatewaySSE stayed
subscribed to the PREVIOUS profile's watcher (switchToProfile never restarted it;
the probe reattach is gated on !_gatewaySSE which is false while the old ES is
open). switchToProfile() now calls startGatewaySSE() after S.activeProfile is set
— it closes the old ES + reconnects with the new profile cookie, self-gating on
_showCliSessions.
* Release PF: scope gateway watcher to active profile (#2848)
rodboev's #3629, re-pushed to fix both bounce findings (client SSE reconnect +
singleton→profile-keyed registry). Two Codex CORE races caught + fixed inline:
(1) stop/subscribe sentinel race, (2) tab's own SSE staying on the old profile
after switch. Codex SAFE + Opus SHIP (architecture) + gateway-watcher tests green.
Co-authored-by: rodboev <rodboev@users.noreply.github.com>
---------
Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
Co-authored-by: rodboev <rodboev@users.noreply.github.com>
Two more rodboev test-only PRs continuing #3664 Windows-harness path
normalization (test_bootstrap_python_selection, test_issue513_wsl_autostart,
test_title_aux_routing). Linux assertions stay at full strictness. Codex SAFE +
full suite 9114 green. No version stamp (tests-only).
Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
Co-authored-by: rodboev <rodboev@users.noreply.github.com>
Same-day releases (OZ/PA/PB/PC/PD/PE) cut each stage before the prior merged, so
sequential squash-merges collapsed their entry content under one heading and
dropped the per-version ## [v0.51.NNN] separators (changelog jumped to 438). All
entry text was intact; this re-inserts the 439-444 headings to match git tags +
moves the tests-only #3664 note to [Unreleased] (no tag). Docs-only.
Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
ai... rodboev's #4133, notification-feature ONLY (dropped the unrelated
'invalidate sidebar cache on WAL' commit — that's a separate good fix to split
out). Button now reflects live Notification.permission instead of silent no-op.
Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
Co-authored-by: rodboev <rodboev@users.noreply.github.com>
* stage-3664: Windows pytest harness hardening (test-only batch #4254/4255/4256/4257/4259)
* test(#3664): Windows pytest-harness compatibility batch
Combines 5 rodboev test-only PRs (#4254/#4255/#4256/#4257/#4259), all under tests/.
Linux assertion paths preserved at full strictness; relaxations + new markers are
Windows/no-fork gated. Codex SAFE + Opus SHIP + changed tests green (cascade-only
full-suite noise is the box artifact). No version stamp (tests-only, Nathan rule).
Co-authored-by: rodboev <rodboev@users.noreply.github.com>
---------
Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
Co-authored-by: rodboev <rodboev@users.noreply.github.com>
* stage-3955: blur composer on Escape for j/k nav + IME guard (Opus nit)
* stage-3955: update test to match IME-guarded blur branch
* Release OX: blur composer on Escape for j/k message nav (#3952)
Self-rebased TomBanksAU's #3955 onto v0.51.436; full Codex+Opus+suite gate.
Escape now blurs the focused composer so j/k session-nav works, after higher-
priority Escape actions run; dropdown-Escape stops propagation; IME-composition
guarded (Opus nit). Also restored the v0.51.434/435 CHANGELOG headings that an
earlier batch left folded under v0.51.436 (hygiene fix).
Co-authored-by: TomBanksAU <TomBanksAU@users.noreply.github.com>
---------
Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
Co-authored-by: TomBanksAU <TomBanksAU@users.noreply.github.com>
* stage-3921: cron-session unread disambiguation (#3460, rodboev)
* Release OW: cron sessions mark unread on new runs (#3460)
Self-rebased rodboev's #3921 onto v0.51.435; full Codex+Opus+suite gate.
Fixes cron-session unread cross-attribution: resolver now considers all known
job ids and attributes each session to the longest matching cron_<jobid>_ prefix
(provably unambiguous). Read-only state.db scan, fires only on real completions.
Co-authored-by: rodboev <rodboev@users.noreply.github.com>
---------
Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
Co-authored-by: rodboev <rodboev@users.noreply.github.com>