Codex re-gate found two issues in the uncapped-read fix: (1) open_anchored_fd
opens blocking O_RDONLY, which HANGS on a workspace FIFO/special file swapped in
at a regular-file path; (2) the non-regular fstat branch returned without
closing the fd (leak). Fix: lstat the symlink-resolved target BEFORE any open to
skip non-regular leaves (no blocking open on a FIFO), and close the fd on the
non-regular fstat branch. Adds a threaded FIFO-no-hang regression test.
Codex regression gate found that routing workspace reads through
read_file_content() introduced a 400KB MAX_FILE_BYTES cap not present on master
(Path.read_text), so a large but legitimate workspace file raised ValueError ->
caught -> None -> the rollback diff falsely reported it as DELETED. Replace with
an uncapped anchored O_NOFOLLOW read (safe_resolve_ws + open_anchored_fd +
S_ISREG fstat check) that keeps the symlink-escape protection but has no size
cap. Adds a >400KB regression test (RED on the capped path, GREEN with the fix).
Co-authored-by: Hinotoi-agent <Hinotoi-agent@users.noreply.github.com>
Absorbs #4412 (tomas-forgac) rebased onto fresh master, plus two
Codex-regression-gate fixes: (1) use the scheme that actually answered the
probe for the ready/already-up URL + browser-open, (2) honor an explicit
HERMES_WEBUI_HEALTH_URL override in the WSL autostart probe.
Co-authored-by: tomas-forgac <tomas-forgac@users.noreply.github.com>
Closes#4412
Codex regression gate found that _session_model_identity_matches normalized
only the @provider:model form, not provider/model (slash). A session stored as
'deepseek/deepseek-v4-1m' resolving to bare 'deepseek-v4-1m' was treated as a
model change -> the 256k fallback bypassed the accept-guard and clobbered the
persisted 1M window (the same #4248 bug on slash-qualified IDs). Mirror the
slash-aware split already used by _model_matches_configured_default. Adds a
route-level regression + unit test for the slash shape.
Co-authored-by: franksong2702 <franksong2702@users.noreply.github.com>
Add readyState !== CLOSED (2) guards to all EventSource.close() calls across
messages.js, sessions.js, panels.js, and terminal.js. Chromium reports
net::ERR_ABORTED when close() is called on an already-closed EventSource,
polluting the console during normal lifecycle transitions (session switch,
panel close, stream teardown).
Key hotspots:
- closeLiveStream() double-close via onerror → _closeSource chain
- _closeSessionEventsSSE, _gatewaySSE.onerror missing try/catch
- _kanbanStopPolling, terminal close handlers
20 close() calls guarded across 4 files, no structural changes to stream
lifecycle.
Restore previously used pathname-based shutil.copy2() which follows an in-tree
symlink whose target escapes the workspace. Route restore writes through the
workspace-anchored file helpers so a restored file always lands inside the
workspace tree. +regression test (escaping symlink refused, normal restore ok).
Co-authored-by: hinotoi-agent <paperlantern.agent@gmail.com>
Pastes over 4000 chars or 100 lines attach as a timestamped pasted-text-*.md in
the upload tray instead of flooding the composer. Image-paste path unchanged; an
oversize paste that exceeds MAX_UPLOAD_BYTES falls through to a normal inline
paste so nothing is lost.
Co-authored-by: santastabber <204424800+santastabber@users.noreply.github.com>
The outline FAB was position:fixed at z-index 9999 and could stack on top of the
scroll-to-bottom button. Anchor it inside the messages shell (position:absolute,
z-index 12) so the two controls don't overlap.
Co-authored-by: Mohamed Habib <Habib1001-m@users.noreply.github.com>
A user prompt that exists only in Hermes state.db (no sidecar copy) and falls
chronologically BEFORE a newer sidecar message tail was being appended after the
tail (or skipped) by merge_session_messages_append_only — losing or mis-ordering
it on load. The merge now inserts such a state.db user message at its correct
chronological position via _insert_state_message_chronologically, with a
role-aware tie-break for equal timestamps. Append-only invariant + dedup
(seen_content_keys) preserved.
Deterministic-ordering guard (maintainer add, addresses Codex review): on an
equal-timestamp collision the insert no longer lands at a slot whose left
neighbour shares the message's role — it advances past the same-role
equal-timestamp run, so an already-answered earlier turn can't be re-ordered
into user,user,assistant. (The agent core merges adjacent users before send, so
this was benign in practice, but the guard keeps the merged transcript correctly
ordered regardless.) +1 same-second multi-turn regression test, toothless-checked.
Co-authored-by: Dennis Soong <dso2ng@gmail.com>
Multi-container Docker deploys died at startup with 'could not create
hermes_agent.egg-info: Permission denied'. docker_init.bash stages the :ro
hermes-agent-src into /tmp/hermes-agent-build to avoid EROFS, but rsync -a/cp -a
preserve mode bits — a source mounted 555 left the staged copy 555, so
setuptools couldn't write egg-info next to the package. chmod -R u+w the staged
tree after the copy (error_exit on failure) so the build dir is genuinely
writable, not just owned by hermeswebui.
Co-authored-by: enihcam <enihcam@users.noreply.github.com>
Follow-up to #4385/#4388: that fix stopped stale cron sidecars from being
treated as CLI rows, but get_cli_sessions() still re-projected the same cron
run from Hermes state.db with a hardcoded archived: false after all_sessions()
omitted the hidden sidecar — so an archived run reappeared on refresh.
- New _state_projection_sidecar_metadata(sid) helper returns UI-owned title +
archived flag from the WebUI sidecar (load_metadata_only), degrading
gracefully to {title:None, archived:False} on any miss.
- Both state.db-projection paths (standard CLI + cron fallback) now use it and
carry the archived flag onto the projected row (was hardcoded False), so an
archived cron/tool/API run stays hidden regardless of which path surfaces it.
Title-preference behavior (#1486 rename persistence) is preserved.
Co-authored-by: TomBanksAU <246584738+TomBanksAU@users.noreply.github.com>
- Hamburger button: 32x32 -> 44x44 tap target (iOS HIG minimum), SVG icon
20 -> 24px. flex-shrink:0 preserved; spacer keeps the title centered.
- Edge-swipe-to-open-sidebar: edge zone 28 -> 80px, trigger 72 -> 64px, max
vertical 48 -> 56px; removes the _isPwaStandalone() gate in
_onPwaSidebarSwipeStart so the gesture works in a normal mobile browser, not
only the installed PWA. Still _isDesktopWidth()-blocked, touch/pen-only,
interactive-target-excluded, and requires horizontal intent (dx>=64 &&
dx>|dy|*1.5), so scroll/tap won't trigger it.
Known tradeoff (Nathan-approved ship-as-is): in mobile BROWSER the 80px left
edge zone overlaps iOS Safari's back-swipe; degrades gracefully (PWA, the
primary mobile target, has no back gesture).
Verified live at 390x844: hamburger renders 44x44 + tap opens sidebar; a
left-edge swipe opens the sidebar in a non-PWA context. 66 existing mobile/
swipe/titlebar tests pass.
Co-authored-by: kaishi00 <kaishi00@users.noreply.github.com>
Paused/disabled cron jobs are partitioned out of the active list into a
collapsible 'Paused (N)' <details> section so they don't drown the active
jobs. _cronList stays the single source of truth — only the render is split,
so openCronDetail / _cronNewJobIds / detail-refresh are untouched. Reuses the
already-translated cron_status_paused label (zero new i18n keys). Collapsed
state persisted in localStorage.
Maintainer polish on absorb: wrapped both localStorage read/write in try/catch
(repo convention — a storage throw in private mode must not dump the cron
sidebar into the error banner) + added the CHANGELOG entry.
Co-authored-by: Sanjay Santhanam <51058514+Sanjays2402@users.noreply.github.com>
The A3 flake (test_sessions_response_backfills_imported_messaging_source_metadata
failing with StopIteration on a sharded CI run while passing in isolation +
sequentially) had two compounding causes, both fixed:
1. ROOT CAUSE — show_cli_sessions setting leak across shard-ordered tests.
33 gateway_sync tests flip show_cli_sessions=True but only ~30 reset it,
and cleanup_test_sessions didn't reset it. Under pytest-shard ordering a
test could run with the setting in an unexpected state, so its session was
occasionally absent from /api/sessions. cleanup_test_sessions now resets
show_cli_sessions to its default (off) after every test, the same way it
already resets last_workspace.
2. SYMPTOM — bare next(generator) with no default raised an opaque
StopIteration (read as a mystery flake) instead of a diagnosable failure
when the session was absent. The 5 bare next() call sites over /api/sessions
results now use next(..., None) + an explicit assertion naming the missing
session, so any future isolation regression fails clearly instead of
masquerading as a flake.
No production code change; tests + fixture only.
Two coherent fixes for the #4346 transcript-virtualization scroll-jump
cluster, shipped together (contributor-approved) because both touch
static/ui.js + the same virtualization test file and interact (the
cache-reset path uses the per-role default from #4367). Both only run when
the default-OFF experimental "Virtualize long transcripts" setting is on.
#4367 — per-role virtual scroll height estimates:
Replaces the flat 140px unmeasured-row estimate with per-role fallbacks
(user 120 / assistant 160 / tool_call 400 / default 140) via
_messageVirtualDefaultHeightForRole + _messageVirtualRoleForEntry, so
tool-call rows (which measure 500-2000px) aren't under-counted before
measurement. Additive + backward-compat (opts.roleForIdx optional → flat
default when absent). The two sibling offset calculators route through the
per-role defaults too.
#4368 — anchor-based scroll compensation during measurement:
Adds _compensateScrollForMeasurementDelta (capture anchor + scrollTop →
render → re-pin scrollTop by the anchor row's measured delta; null/missing/
<2px guards; programmatic-scroll flag managed) wrapping
renderMessages({preserveScroll:true}) in _scheduleMessageVirtualizedRender,
plus a 150ms scroll-active guard deferring measurement refresh while the user
scrolls, and session-switch cleanup of the three scroll-settle globals.
Maintainer note: combining the two required the test harness for
test_clear_height_cache_resets_scroll_settle_globals (#4368) to also eval
_messageVirtualDefaultHeightForRole + define MESSAGE_VIRTUAL_DEFAULT_ROW_HEIGHTS
(#4367), since the combined _clearMessageVirtualHeightCache resets the estimate
via the per-role helper. Both PRs' test sets retained.
Co-authored-by: Rod Boev <rod.boev@gmail.com>