Commit Graph
100 Commits
Author SHA1 Message Date
nesquena-hermes e061e8534c Rebase #5337 (public read-only conversation sharing) onto master
Rebased 363 commits forward (branch-point 2026-07-05). Applied the PR's code
diff cleanly across 16 files; resolved api/models.py additively (both master's
process_wakeup_pause/anchor_scene_index fields AND the PR's share_token/
share_created_at fields kept in the Session constructor, metadata list, and
to_dict). CI workflow (.github/workflows/tests.yml) intentionally NOT taken from
the stale branch — master owns it (the branch's apparent removal of Playwright/
Office steps was a PAT-workflow-scope revert; rebase restores master's CI).

Co-authored-by: MicoRobot. Concept approved by maintainer (opt-in public share,
2026-07-11). Note: 4 dynamic share tests fail identically on the original
unrebased head (pre-existing test-env Session.load fragility, not rebase-caused);
to be resolved during the security re-gate in a proper sandbox.
2026-07-11 01:39:42 +00:00
nesquena-hermes 40db97733e Release exp: worklog event timestamps (#5739). Thanks @rodboev. 2026-07-11 00:43:53 +00:00
nesquena-hermes bdb245346d Merge #5739: worklog event timestamps (+ gate fixes) 2026-07-11 00:43:24 +00:00
nesquena-hermesandrodboev. 97fe772fdb fix(worklog): add full-date tooltip + hide event-time under 420px (Fable UX)
Two non-blocking UX improvements from the Fable gate on #5739:
1. Full date+time tooltip (title attr) on the worklog event-time label — the bare
   clock is date-ambiguous when a settled session is reviewed later or a run crosses
   midnight. New _activityFullClockLabel (range-guarded so a bad epoch never yields
   'Invalid Date' in the tooltip) + test.
2. Hide the time label under 420px — its ~50px of fixed chrome squeezes the
   collapsed row's flexible preview toward zero on phones.

Co-authored-by: rodboev.
2026-07-11 00:38:23 +00:00
nesquena-hermesandrodboev. Gate findings from adversarial Codex re-gate. a86324985e fix(worklog): persist event timestamps through settlement + reject invalid epochs
Two gate findings on the worklog-event-timestamp feature (#5700/#5739):

1. Thinking-row timestamps disappeared after settlement. Reasoning source events
   carry no server timestamp; the live DOM showed a fallback time but the settled
   scene row rebuilt with created_at:null. Now stamp created_at on the source event
   the first time it's seen (messages.js apply path) and carry row.created_at
   through snapshot hydration, so the timestamp survives live->settled.

2. A garbage huge numeric timestamp (e.g. 1e20) passed the finite/>0 checks and
   rendered literal 'Invalid Date'. _timestampSeconds now rejects epochs outside
   JavaScript's valid Date range. Added a regression test.

Co-authored-by: rodboev. Gate findings from adversarial Codex re-gate.
2026-07-11 00:23:48 +00:00
nesquena-hermes d07e41d572 Release exp: final answer only activity display mode (#5746)
Stamp CHANGELOG for the #5746 ship. Thanks @rodboev.
2026-07-10 23:51:52 +00:00
nesquena-hermes 0af0a5fcf0 Merge #5746: final answer only activity display mode 2026-07-10 23:50:28 +00:00
nesquena-hermes d5210b7efc lint: drop now-unused importlib.util import 2026-07-06 21:19:20 +00:00
nesquena-hermes acfd0e9719 fix: defensive agent-importable probe (find_spec raises ValueError on spec-less partial agent module on CI) 2026-07-06 21:14:58 +00:00
nesquena-hermes 88d94556d2 docs(changelog): stamp last-2-flakes fix 2026-07-06 21:10:00 +00:00
nesquena-hermes 44be49ebc7 test: fix chronic full-suite hermes_cli __path__ isolation poison (8 flakes) 2026-07-06 19:53:32 +00:00
nesquena-hermes b08e6d91af docs(changelog): stamp #5685 pinned tail jitter fix 2026-07-06 17:24:26 +00:00
nesquena-hermes ffe6163be8 Merge PR #5685 into stage 2026-07-06 17:23:43 +00:00
nesquena-hermes 21755195ee docs(changelog): stamp #5673 proxy-auth PWA caveat docs 2026-07-06 17:05:11 +00:00
nesquena-hermes 8a7d3eb6c2 Merge PR #5673 into stage 2026-07-06 17:04:53 +00:00
nesquena-hermes ff74ea7993 docs(changelog): stamp #5668 layout-assertion test helpers 2026-07-06 06:13:51 +00:00
nesquena-hermes 358c77d775 Merge PR #5668 into stage 2026-07-06 06:13:26 +00:00
nesquena-hermes f9ca38dd5c docs(changelog): stamp visible batch (#5605, #5560, #5630, #5633, #5594) 2026-07-06 05:38:46 +00:00
nesquena-hermes 51dffad1ae Merge PR #5594 into visible-batch stage 2026-07-06 05:37:45 +00:00
nesquena-hermes 25e32839a3 Merge PR #5633 into visible-batch stage 2026-07-06 05:37:45 +00:00
nesquena-hermes db87fd3a07 Merge PR #5630 into visible-batch stage 2026-07-06 05:37:45 +00:00
nesquena-hermes a2e5dff18e Merge PR #5560 into visible-batch stage 2026-07-06 05:37:44 +00:00
nesquena-hermes 49afd5c0c2 Merge PR #5605 into visible-batch stage 2026-07-06 05:37:44 +00:00
nesquena-hermes e3dfc29950 docs(changelog): stamp phase1 batch (#5644, #5653, #5661) 2026-07-06 03:53:18 +00:00
nesquena-hermes 6416192ceb Merge PR #5644 into phase1 stage 2026-07-06 03:52:38 +00:00
nesquena-hermes f727df5891 Merge PR #5653 into phase1 stage 2026-07-06 03:52:38 +00:00
nesquena-hermes 5f396bb7d6 Merge PR #5661 into phase1 stage 2026-07-06 03:52:38 +00:00
nesquena-hermesandrodboev dbfa33a999 feat(moa): route /moa through WebUI via server-resolved MoA config (#5070, #5057)
Adds a dedicated GET /api/commands/moa/resolve endpoint + resolve_moa_config()
that reads the MoA preset/usage server-side. The /moa send-path passes only a
moa_config:true boolean to /api/chat/start; the server re-resolves the real
config itself (never trusts a browser-supplied dict) and threads it per-turn into
the live agent run. Fails closed (409) on gateway-backed sessions.

Replaces the originally-proposed broad 43-command subprocess approach (bounced)
with this narrow, security-reviewed /moa route. Maintainer fixes on top of the
contributor's converged design: moa_config is added to run_conversation only
when not None (older-agent TypeError safety) at the main call AND both auth
self-heal retry sites, and forwarded through the legacy-journal runtime adapter.

Gate: Codex SAFE (after the maintainer fixes) + full suite 10784 pass + the
activity-stream regression gate clean.

Co-authored-by: rodboev <rodboev@users.noreply.github.com>
2026-06-28 00:23:51 +00:00
nesquena-hermesandgottipx 3c3c9047bb skins: add GitHub, Codex, and Terracotta opt-in appearance skins (#4634, #4636, #4635)
GitHub (#4634): graphite + Primer-blue. Codex (#4636): minimal editor, sage accent.
Terracotta (#4635): warm clay accent, renamed from the originally-proposed name to a
descriptive material name. All CSS-only, namespaced under [data-skin], opt-in via
Settings > Appearance. Gate: Codex SAFE + full suite 10778 pass + 3 accents verified live.

Co-authored-by: gottipx <gottipx@users.noreply.github.com>
2026-06-27 23:57:37 +00:00
nesquena-hermesandfranksong2702 e2ade27bfe fix(anchor): preserve settled mixed content order (#4958)
When a settled assistant turn carries _anchor_activity_scene and has mixed
content[] (text interleaved with tool_use), promote that ordering into anchor
scene rows (prose / tool / thinking rows in sequence) instead of only the raw
content[] fallback, and mirror the same scene model in the backend hydration
path so a cold reload reconstructs an identical transcript.

Also fixes: post-tool_use text/thinking in a NON-FINAL assistant message was
silently dropped — only the turn-final assistant's post-last-tool text is the
"final answer" tail; earlier assistant messages keep post-tool content as
activity rows. Tail thinking is excluded from the final-answer text but still
emitted as a thinking row (verified on both JS and Python paths).

Tool-row de-duplication is conservative — ID-equal always merges; name/
invocation matches are cardinality-gated and same-message; body-prefix is only
a secondary confirmation inside the id-flexible branch (also requires matching
started_at + compatible name + invocation). It biases toward an extra visible
tool card over ever silently merging two distinct calls.

Gate: Codex SAFE (tail-thinking edge verified already-handled both sides),
Opus SHIP (dedup fails toward duplicate-not-loss; non-final preservation, XSS,
non-mixed regression all clean), full suite 10778 passed (only the 4 known
pre-existing flakes). One non-blocking follow-up noted: bring the prose/thinking
peer-dedup into JS↔Python parity (cosmetic cold-reload extra-narration row).

Co-authored-by: franksong2702 <franksong2702@users.noreply.github.com>
2026-06-27 22:54:29 +00:00
nesquena-hermes 919901265f test(#4986): drop unused imports (ruff F401) 2026-06-27 21:49:23 +00:00
nesquena-hermesandhyl-ailab f7d727d061 fix(compress): make manual /compress durable across reload + restart (#4986)
Manual /compress shrinks the model-facing context_messages but keeps the
visible messages[] transcript. The old context returned via two paths (#4836):
(1) append-only state.db reconciliation re-appending pre-compression rows, and
(2) startup .bak recovery treating the intentional shrink as data loss.

Half A (routes.py _handle_session_compress): persist truncation_watermark +
truncation_boundary (= watermark of the compressed context), set
compression_anchor_mode="manual", refresh last_prompt_tokens, stamp missing
timestamps on the compressed context, and delete the now-stale .bak. The
boundary==watermark stamp drives the #4772 reconciliation logic onto its
conservative path (block replay of pre-compression rows) while post-compression
turns still merge once sidecar timestamps advance past the watermark.

Half B (session_recovery.py): the .bak recovery guard. MAINTAINER FIX over the
original PR — the contributor suppressed recovery whenever
compression_anchor_mode=="manual", a flag set once at compress and never
cleared, which PERMANENTLY disabled #1558 crash-recovery for any compressed
session (real data loss). Recovery is now suppressed only when the session was
intentionally compressed AND the .bak is genuinely the pre-compression backup,
discriminated by the compaction marker (the same _context_messages_include_
compression_marker signal reconciliation uses): a marked .bak post-dates the
compression -> recover; an unmarked .bak with a larger context is the
shrink-undoing pre-compression one -> suppress. Fail-open on any error.

Resolves an Opus-gate edge in the first cut (length-only heuristic wrongly
suppressed a loss that shrank BOTH messages and context). Two new non-vacuous
regression tests: post-compression real loss recovers, and the both-shrunk
marked-backup case recovers.

Co-authored-by: hyl-ailab <hyl-ailab@users.noreply.github.com>
2026-06-27 21:43:27 +00:00
nesquena-hermesandrodboev 519c394dfe fix(boot): preserve typed composer input during saved-session boot restore (#5069)
On a hard refresh, boot-restore auto-opens the last session from localStorage,
but that load can finish after the user has started typing into the fresh
composer — and the saved-session draft restore clobbered the typed input.
_restoreComposerDraft already had a preserveActiveInput guard, but the boot
path never set it.

boot.js now calls loadSession(saved, {preserveActiveInput:true}); loadSession
threads it into the draft restore as
preserveActiveInput:!!opts.preserveActiveInput || (currentSid===sid&&forceReload),
preserving the prior same-session-force-reload behavior. A blank composer still
restores the saved draft (the guard only skips when active input is non-empty
and differs). opts is read via arguments[1] so the call is signature-safe.

The 4 one-line test changes refresh exact-string boot snapshots to the new call
signature; test_issue5060_boot_composer_preserve.py adds behavioral coverage
(2 assertions proven RED on master).

Co-authored-by: rodboev <rodboev@users.noreply.github.com>
2026-06-27 20:51:46 +00:00
nesquena-hermesandrodboev 5bb03a6322 feat(settings): sync a Hermes Dashboard chip with Appearance dropdown (#4936)
Adds a "Hermes Dashboard" chip to the Settings > Appearance "Sidebar tabs"
grid, bidirectionally synced with the existing dashboard-mode dropdown
(never/always/auto). Chip off -> dropdown "never"; chip on -> restore last
non-never mode; dropdown change -> chip re-render.

The chip is a proxy for the dashboard config via saveDashboardSettings ->
POST /api/dashboard/config; it NEVER mutates hidden_tabs/tab_order (separate
toggle fn from _toggleTabVisibilityChip). loadDashboardSettings guards a stale
config GET from clobbering a newer save (_dashboardSettingsLoadSeq /
_dashboardSettingsWriteSeq), and a failed chip save rolls the dropdown/chip
back. Chip label via textContent (XSS-safe).

Co-authored-by: rodboev <rodboev@users.noreply.github.com>
2026-06-27 20:33:51 +00:00
nesquena-hermesandrodboev 20d0313d0c feat(settings): expose max_tokens override in Settings (#5010)
The streaming path already honored a max_tokens cap from the active profile's
config.yaml, but there was no UI to manage it. Settings > Preferences now has a
"Max output tokens" field that writes a root-level max_tokens override through
the profile config (not settings.json, which the streaming worker never reads
for this), preserving unrelated YAML keys under _cfg_lock. Blank clears the
override so agent.max_tokens fallback resumes; get_max_tokens_status() surfaces
root / effective / fallback so the field shows what a new turn would use.

Co-authored-by: rodboev <rodboev@users.noreply.github.com>
2026-06-27 20:27:18 +00:00
nesquena-hermesandrodboev 3ad07ba363 fix(agent_health): read cross-container gateway runtime status from gateway_state.json (#5065)
In cross-container Docker setups the agent-health check resolves the gateway's
runtime status from a shared gateway_state.json. After the keyword-form
read_runtime_status(pid_path=...) call fails against an older agent signature,
_read_gateway_runtime_status fell back to read_runtime_status(pid_path) passing
the sibling gateway.pid path positionally — so the reader parsed PID metadata as
runtime metadata and gateway_state/updated_at never resolved, breaking the
freshness check (issue #5030, #1879).

The positional fallback now passes the correct gateway_state.json sibling path
(pid_path.with_name(runtime_status_file), where runtime_status_file comes from
the agent's _RUNTIME_STATUS_FILE attr, defaulting to gateway_state.json). The
path computation is hoisted so both fallback branches use the correct file. The
keyword happy-path is unchanged for agents that accept pid_path=..., and normal
PID-based health is unaffected.

Verified: the new test test_cross_container_runtime_status_reads_sibling_runtime_file
fails on master without the fix and passes with it; #716 PID-health tests still pass.

Co-authored-by: rodboev <rodboev@users.noreply.github.com>
2026-06-27 19:54:50 +00:00
nesquena-hermesandStacey2911 97a25a98fb fix(streaming): strip consumed OOB steer blocks from model-facing history (#4978)
When a turn is steered, the agent embeds an
[OUT-OF-BAND USER MESSAGE]...[/OUT-OF-BAND USER MESSAGE] control block, and that
block gets persisted into the session's messages/context_messages (appended onto
a tool-result entry — verified present in real prod session JSON, in both
messages[] and context_messages[]). WebUI then replays context_messages into the
provider-facing conversation history every turn, so the model kept re-seeing the
internal control marker: wasted tokens + stale control data.

Adds a non-mutating, recursive _strip_oob_blocks() (regex, DOTALL+IGNORECASE,
non-greedy) wired into every model-facing chokepoint:
_sanitize_messages_for_api, _api_safe_message_positions,
_restore_reasoning_metadata projection, and the gateway runs-API
conversation_history (context_messages + prefill). The compression path
(/session/compress) inherits the strip via _sanitize_messages_for_api.

The marker stays intact in the persisted transcript (display/reload/audit
fidelity); it's removed only on the way to the provider. Idempotent, only strips
a complete open+close pair, and does not mutate persisted session objects (the
content key is reassigned to a freshly-built value).

Maintainer hardening (Opus gate): relaxed the opening pattern from a mandatory
`]\r?\n` to `]\s*?` so a single-line OOB block is also stripped (DOTALL .*?
already spans newlines) — robustness against a format that omits the newline.

Co-authored-by: Stacey2911 <Stacey2911@users.noreply.github.com>
2026-06-27 19:13:36 +00:00
nesquena-hermesandallenliang2022 4f64443f63 fix(sessions): reload active transcript only on message-count change (#5061)
The active-session external-refresh check (refreshActiveSessionIfExternallyUpdated)
force-reloaded the whole transcript whenever last_message_at/updated_at advanced.
The post-turn background skill/memory review bumps those timestamps without adding
any chat messages, so the destructive loadSession(force) (clears S.messages, awaits
a round-trip) made the open conversation visibly disappear and reappear with no new
content.

The refresh now reloads only when the message count actually changed, and treats a
timestamp-only bump as metadata (advance the local last-seen marker + refresh the
lightweight sidebar list, no transcript teardown).

Maintainer fix on top of the contributor's PR (Codex gate): the count check fires
in BOTH directions — remoteCount !== localCount, not just >. A LOWER remote count
means another tab/client truncated/undid/retried/regenerated the transcript
(/api/session/truncate, /retry, /undo all shrink s.messages and persist a lower
message_count); the original >-only gate would have treated that as metadata-only
and silently kept the stale longer transcript forever. Added a regression test for
the count-decrease case. The pending-user-message count-inflation path is excluded
by the existing busy/active-stream guards.

Co-authored-by: allenliang2022 <allenliang2022@users.noreply.github.com>
2026-06-27 18:49:08 +00:00
nesquena-hermesandfranksong2702 3c493a9e37 fix(cancel): preserve partial turn payload via embedded cancel snapshot (#4647)
Cancelling a running turn briefly collapsed the already-streamed reasoning,
tool cards, and text to a bare "Task cancelled" marker before a follow-up
GET /api/session repopulated them — a visible race between the terminal
cancel SSE event and the refetch (#4076, #1361).

The terminal cancel event now carries a canonical, redacted session snapshot
captured under the per-session agent lock right after the cancelled turn is
persisted (including the recovered partial-assistant message). The frontend
applies that embedded snapshot immediately via _applyCancelSessionPayload and
skips the GET round-trip. The snapshot is a fully detached deep copy (redaction
rebuilds every container) so it cannot alias or be staled by concurrent writes;
it is only applied when its session_id matches the active view; and a
missing/failed snapshot degrades cleanly to the existing GET fallback.

The seven other cancel call sites are standardized via _cancel_event_payload()
(message + type + status), backward-compatible (no session key when none).

Co-authored-by: franksong2702 <franksong2702@users.noreply.github.com>
2026-06-27 16:43:37 +00:00
nesquena-hermes 3abd17f51d Release: read-side sidecar self-heal from newer state.db, lock-safe (#5053) 2026-06-27 15:34:35 +00:00
nesquena-hermes 5493ec1e7f Release: coalesce session-visit model refreshes with bounded latency + hang-safe followers (#5051, fixes #5047) 2026-06-27 13:45:08 +00:00
nesquena-hermes e7cd8f5f12 Release: memory-section hover path titles (#5050, fixes #5045) 2026-06-27 10:52:29 +00:00
nesquena-hermes e47c336981 Release: relay mirrored gateway approvals after stream loss (#5041, fixes #5000) 2026-06-27 06:14:26 +00:00
nesquena-hermes 470769454f Release: extension post-install next steps in gallery (#4964, fixes #4959) 2026-06-27 05:53:40 +00:00
nesquena-hermesandfranksong2702 11260c8df5 feat(extensions): show post-install next steps in the gallery (#4964)
Renders generic registry post_install guidance + lifecycle requirements in the
Settings extensions gallery card (manifest-driven, no vendor special-casing).
Fixes #4959. Rebased onto current master (squash-applied #4964's net diff so it
coexists with the #4979 sidecar-runtime-status feature that landed in v0.51.697).

Co-authored-by: franksong2702 <franksong2702@users.noreply.github.com>
2026-06-27 05:44:10 +00:00
nesquena-hermes 0825612a59 Release: extension sidecar runtime status in diagnostics panel (#4979) 2026-06-27 05:33:40 +00:00
nesquena-hermesandfranksong2702 64e877b432 fix(extensions): don't preserve loading/error placeholder as existing diagnostics
Codex gate finding on #4979: loadExtensionsPanel({preserveExisting:true}) on the
diagnostics tab treated the 'Loading extension diagnostics…' placeholder as real
content, so a failed refresh returned early (preserveExisting && innerHTML.trim())
and left the panel stuck on 'Loading…' instead of rendering the error. Exclude the
.extensions-loading/.extensions-error placeholders from the preserve check + add a
regression assertion.

Co-authored-by: franksong2702 <franksong2702@users.noreply.github.com>
2026-06-27 05:21:45 +00:00
nesquena-hermes 1035c24fb2 Release: completion notification previews settled final answer (#5036, fixes #5035) 2026-06-27 05:03:17 +00:00
nesquena-hermes cee278391e Release: onboarding searchable model picker (#5031, fixes #4706) 2026-06-27 04:42:14 +00:00
nesquena-hermesandrodboev 0a32fb899f fix(onboarding): rehydrate plain-select fallback so saved model isn't reset to option[0]
Codex gate finding on #5031: when _mountSearchableModelSelect is absent the
fallback returns the old plain <select>, but the post-render rehydration
(modelSel.value = ONBOARDING.form.model) was removed — so a saved/default model
that isn't the first option would be silently replaced by option[0] on render.
Restore the rehydration in an else branch + add a regression test.

Co-authored-by: rodboev <rodboev@users.noreply.github.com>
2026-06-27 04:33:02 +00:00
nesquena-hermes 532ac22e77 Release: French localization completion (#4876) 2026-06-27 04:10:26 +00:00
nesquena-hermesandPichatu 1463aedd69 fix(i18n): rebase French translation onto master — union-merge new keys, restore theme_usage nbsp, bump open_in_vscode locale count 13->14 (fr gap closed)
Co-authored-by: Pichatu <4161398+Pichatu@users.noreply.github.com>
2026-06-27 04:09:36 +00:00
nesquena-hermes dee0080951 Release: tier-1 batch (#4888, #5025, #4997) — graphite chip contrast, memory file paths, mobile dashboard link hide 2026-06-27 03:49:53 +00:00
nesquena-hermes 1ff3f2f3ac Merge PR #4997 into tier-1 batch 2026-06-27 03:39:10 +00:00
nesquena-hermes 298f7af562 Merge PR #5025 into tier-1 batch 2026-06-27 03:39:10 +00:00
nesquena-hermes 59f14f05cd Merge PR #4888 into tier-1 batch 2026-06-27 03:39:10 +00:00
nesquena-hermesandb3nw 74cee77738 fix(approval): stale local approval card clears instead of dead-ending (#4948 local variant, #4771 follow-up)
On the default local in-process backend, an approval card whose stream
ended while still pending (cancel/fork/provider-error/completion) left the
agent queue entry dropped and the _pending mirror reconciled away. Clicking
Approve/Deny then sent an approval_id that matched nothing, so the handler
returned a bare {ok:false}; since #4771 the frontend surfaces that as
'Approval response not accepted.' with a stuck card (reported by
@santastabber on v0.51.666 and b3nw).

Distinguish a genuinely stale card (no pending approval for the session ->
benign {ok:true, stale_cleared:true} so the UI clears the orphan) from a
stale-id click made while a DIFFERENT approval is live (still ok:false so it
can never resolve the wrong command -- #527 guard preserved). Frontend
clears the orphan card on stale_cleared even if the displayed id drifted.

Co-authored-by: b3nw <b3nw@users.noreply.github.com>
2026-06-26 06:46:22 +00:00
nesquena-hermes b87a605306 perf(#4842): freeze the CLI/cron sidebar cache key during streaming
/api/sessions re-ran the expensive CLI/cron session projection on every
poll while a turn was streaming — a major cause of the multi-second sidebar
latency + 100% CPU on cron-heavy installs (continuing #4672/#4808/#4889).

Root cause: the CLI/cron projection is gated by _CLI_SESSIONS_CACHE, whose
key folds in _sqlite_file_stat_cache_key -> _sqlite_content_fingerprint
(MAX(rowid) FROM messages). During a live turn the gateway writes a message
row per streamed delta, so that fingerprint advances on essentially every
~5s poll — busting the cache and re-running the full candidate-join +
projection (and the lineage-metadata pass) on every poll, contending for the
same SQLite/global lock the streaming worker holds.

The route-level session-list cache already froze its key during streaming
(#4808 _session_list_cache_streaming_freeze_marker), but that freeze never
reached this inner CLI-sessions cache, so the heavy CLI/cron query still
re-ran whenever the outer cache validated.

Fix: while any stream is active, the CLI-sessions cache key folds in the same
stable streaming-freeze marker (keyed only on the set of active stream ids)
instead of the volatile content fingerprint, and the cache TTL widens to a
streaming window. The projection is reused across polls mid-stream and
rebuilt at most once per window. The instant a stream starts/stops the marker
changes, so freshly-finished rows surface promptly. Structural mutations
(cron completion, new/renamed/archived sessions, attention) now also clear
the CLI cache via the existing session-list-changed listener — those signals
never fire per streamed token, which is what makes the freeze safe.

Idle behavior is byte-identical (freeze only engages with active streams).
Applies to both the active-profile and all-profiles cache paths.

Adds tests/test_issue4842_cli_sessions_streaming_freeze.py (6 tests): marker
idle/stable/changes semantics, the core 'key stable across message writes
while streaming' guarantee, idle key still advances with the fingerprint,
streaming TTL > idle TTL, and the structural-mutation listener clears the
CLI cache.
2026-06-25 21:57:36 +00:00
nesquena-hermes 0562bdbd31 fix(#4933): one-click extension install works with no env (bootstrap managed default root)
Settings -> Extensions offered an Install button even when extensions were
unconfigured, then dead-ended on 'Extensions not configured' because
install_extension required HERMES_WEBUI_EXTENSION_DIR to be set to an existing
directory. Default to a WebUI-managed root under STATE_DIR/extensions and
bootstrap it on first install so install just works with zero config; the env
var remains an optional override (never auto-created). Gallery-installed
extensions already auto-load via the runtime manifest, so the extension loads
on the next app-shell render with no further setup.

- api/extensions.py: _default_extension_root + _writable_extension_root;
  _extension_root/_extension_root_status fall back to the managed default;
  install_extension bootstraps via _writable_extension_root; dir-unavailable
  warning only fires for an explicitly-set-but-broken env path.
- tests: update disabled-by-default expectations (configured=True, valid=False
  pre-install) + new regression proving install bootstraps + auto-loads with
  no env.
- docs/EXTENSIONS.md: document zero-config one-click install + env now optional.
- CHANGELOG [Unreleased].
2026-06-25 18:45:09 +00:00
nesquena-hermes 8d2d8b1f98 fix(#4622): trim dead enrichment branches per Opus gate
Opus + Codex both SAFE TO SHIP. Opus flagged two cosmetic redundancies (not
blockers): the snippet guard checked payload.result/payload.output which
_anchorSceneToolRowFromCall never sets, and the tool.result/tool.output writes
are never read by the consumer _anchorSceneToolCallFromRow (it reads
tool.snippet||payload.snippet||payload.result||payload.output). Removed both —
tool.snippet/payload.snippet is what actually restores the visible body.
Behavior-identical; 18 settled/reconnect tests still green.
2026-06-25 06:40:27 +00:00
nesquena-hermes ef3092434f fix(#4622): restore tool-card result body on the settled rebuild path
In the Transparent Stream / Worklog view, terminal tool cards lost their stdout
and patch/edit cards lost their diff AFTER a turn settled (live cards were fine).

Root cause (proven via a node harness driving the real settled chain
_anchorSceneRowsByMessageIndex -> _anchorSceneToolRowFromCall ->
_anchorSceneToolCallFromRow): the settled rebuild builds each tool row from the
persisted messages[].tool_calls (state.db/sidecar), which can carry only a short
preview — or, on a cold/paginated load, nothing — for the result body. The full
output lives on the live S.toolCalls entry at settle time, but the dedup loop in
_anchorSceneRowsByMessageIndex SKIPPED the matching live entry (continue) instead
of restoring the missing body onto the surviving settled row. So buildToolCard
got tc.snippet='' -> empty output region, and the patch/edit diff (detected from
the snippet) never rendered. This also blanks the transparent 'Output' tab and
the 'Full' (detailed-input) tab.

Fix: new _enrichSettledToolRowBodyFromLive() restores snippet/result/output,
command, and args onto a settled row from the matched live call, ONLY when the
settled value is empty (never clobbers a genuinely persisted body). Wired into
the dedup loop: on a tid match that would have been skipped, enrich the existing
pooled row instead of dropping the live data.

The prior #4625 guard only exercised the LIVE buildToolCard path, which is why it
stayed green while the settled path regressed. New
tests/test_issue4622_settled_toolcard_body.py drives the full SETTLED chain and
asserts: terminal stdout restored + full output in data-full + transparent Output
tab non-empty; patch diff body restored + renders as a diff + Full-tab args kept;
genuine persisted body not clobbered; args restored when dropped; no-live-match
leaves the row untouched; no duplicate row after enrich. Verified to FAIL on clean
master (assertions) and pass with the fix. Also registered the new helper in the
and now references the new symbol).

Reusable proof harness saved to ~/WebUI/qa/harnesses/.

Closes #4622.
2026-06-25 06:40:27 +00:00
nesquena-hermes 3acd802588 fix(#4856): mobile transcript collapses unscrollable — overflow-x:hidden coerces overflow-y:auto
The mobile .messages-inner rule used overflow-x:hidden, which per the CSS
overflow spec coerces overflow-y from visible to auto — turning the inner
into a scroll container. A scroll container's min-height:auto resolves to 0,
so the inner (a flex-shrink:1 item inside the .messages column flexbox)
collapsed to the viewport height instead of growing to its content. The
transcript then overflowed the clipped inner rather than the .messages
scroller, leaving nothing to scroll and pinning scrollTop at 0 — every
open/send/receive stranded mobile users at the top with no way to scroll down.

Fix: overflow-x:clip suppresses horizontal overflow (the original #4583/#4553
intent) WITHOUT coercing overflow-y or creating a scroll container, so
min-height:auto stays min-content and the inner grows to full height.

Verified empirically via headless Chrome mobile emulation (390x844, mobile=true):
scrollableDelta 0 -> 7746; open/send/receive all land at bottom; mid-scroll
position preserved across re-render. Desktop unaffected.

Updated #4553 test (clip satisfies its horizontal-pan goal) + added dedicated
2026-06-25 05:18:32 +00:00
nesquena-hermes b149a975c6 fix(#4842): update #4385 archive tests to real sidecar files + document stat-gate assumption
Opus gate finding (BLOCKING): the two #4385 archive-reappears regression tests
mocked Session.load_metadata_only to return an archived stub WITHOUT writing a
real sidecar file, and never set SESSION_DIR. The new stat-gate in
_state_projection_sidecar_metadata short-circuits on SESSION_DIR/{sid}.json
before consulting metadata, so those mocks were never reached -> archived=False.

This is NOT a production regression (in prod, load_metadata_only reads exactly
SESSION_DIR/{sid}.json and returns None when absent, so archived metadata only
exists when the file does — in which case the stat() succeeds and behavior is
identical). The divergence existed only at the tests' mock boundary.

Fix: both tests now write a real archived sidecar JSON under a monkeypatched
SESSION_DIR, exercising the actual production read path (strictly better
coverage than mocking load_metadata_only). Also documented the
SESSION_DIR-as-sole-source assumption on the cache function per Opus's
forward-looking note.
2026-06-25 04:56:00 +00:00
nesquena-hermes a6d8319acb perf(#4842): cache sidebar-projection sidecar reads + hoist per-row I/O on the cron path
GET /api/sessions pinned CPU to 100% and took multiple seconds on profiles with
many cron sessions. The state.db -> sidebar projection did three pieces of
redundant per-row file I/O, in BOTH the visible pass and the higher-capped
(CRON_PROJECT_CHIP_LIMIT=200) cron-only second pass:

1. _state_projection_sidecar_metadata(sid): uncached open() + 64KB prefix read +
   JSON-key scan, once per row.
2. get_last_workspace(): up to two file reads + an is_dir()/remote probe,
   returning the SAME workspace for every row.
3. cron/jobs.json: full read + parse per untitled cron row.

On a cron-heavy install that is hundreds of file reads per build, and because
the enclosing _CLI_SESSIONS_CACHE is keyed on a state.db content fingerprint
that advances on every streamed message row, the whole scan was re-paid on
essentially every 5s poll during a live turn (the recurring 100% CPU / slow
/api/sessions from #4672 -> #4808 -> #4842).

Fix:
- Memoize _state_projection_sidecar_metadata by the sidecar's
  (path, mtime_ns, size, ctime_ns) stat signature in a bounded module cache, so
  a warm build re-stats instead of re-reads. A rename/archive/edit bumps the
  signature and invalidates just that one entry (no staleness). This survives
  the 5s CLI-cache busting during streaming, which is the actual trigger.
  Mirrors the Claude Code parse cache (#4718). Folded into
  clear_cli_sessions_cache() for explicit-reset/test parity.
- Build-scoped memos for cron jobs.json (parse once -> job_id->name map) and
  get_last_workspace() (resolve once), matching the existing _cron_pid() pattern.

Output is byte-identical. Adds tests/test_issue4842_cron_projection_perf.py
(7 tests: warm-rebuild no re-read, stat-change invalidation, dict-copy isolation,
missing-sidecar default, jobs.json once, workspace once, lifecycle clear).
2026-06-25 04:56:00 +00:00
nesquena-hermes d61ca37bd1 fix(#4718): harden parse-cache key with ctime_ns + preserve epoch-0 mtime fallback
Dual-gate (Codex + Opus) findings on the Claude Code transcript parse cache:

1. Cache-key staleness hardening: add st_ctime_ns to the (path, mtime_ns, size,
   max_messages) key so a rare same-size, same-mtime in-place edit can't serve a
   stale parse. Size is already the strong discriminator for append-only JSONL;
   ctime_ns is cheap defense-in-depth (a spurious bump costs one harmless re-parse,
   never a stale read).

2. Timestamp-fallback bug-for-bug fix: the row-builder guard used identity
   (first_ts is None and last_ts is None) but the field assignment uses truthiness
   (first_ts or last_ts or _mtime). They disagreed on a falsy 0.0 timestamp
   (epoch-0/1970 transcripts), leaving created_at/updated_at/last_message_at = None
   instead of the file mtime the pre-cache inline code fell back to. Guard now uses
   the same 'not first_ts and not last_ts' test.

Adds 3 regression tests: epoch-0 mtime fallback, the shared-dict read-only
contract (pins the load-bearing invariant), and same-size+mtime ctime invalidation.
2026-06-25 02:46:00 +00:00
nesquena-hermesandrodboev fdb734a6a0 perf(#4718): cache Claude Code transcript parses on the sidebar path
The /api/sessions cold build (which a profile switch triggers) was
dominated by get_claude_code_sessions() re-parsing every Claude Code
JSONL transcript line-by-line on each call — 650-1000ms on a 200-file
/ ~130MB tree, repeated on every switch, every 5s CLI-cache expiry, and
every sidebar poll, because the directory is global but the higher
session cache is keyed per active profile.

Memoize the per-file parse by (path, mtime_ns, size) so a warm build
re-stats files instead of re-parsing them; any genuine edit/append
changes the stat signature and invalidates just that file. Also hoist
the loop-invariant get_last_workspace() out of the Claude Code row loop
(it stats config + probes terminal cwd, ~200 redundant calls per build).

Measured /api/sessions cold-rebuild on a 250-session profile: ~670-880ms
-> ~100-200ms (verified in-browser). Output byte-identical.

Credit @rodboev for the browser-level profiling that isolated the real
bottleneck (the prior pre-warm hypothesis could not move it).

Co-authored-by: rodboev <rodboev@users.noreply.github.com>
2026-06-25 02:45:59 +00:00
nesquena-hermes 36c92d8bd2 Release WH (v0.51.627): cron Task APIs survive a shadowing top-level cron package (#4855) 2026-06-24 17:53:14 +00:00
nesquena-hermes caf36d11dd Release WG (v0.51.626): restore mobile streaming scroll-jank guard (#4857) 2026-06-24 17:40:30 +00:00
nesquena-hermes f84db40a23 test(#4813): resolve node via shutil.which + skip if absent (CI portability)
The hardcoded /home/hermes/.local/bin/node doesn't exist in CI runners. Use
shutil.which('node') with a module-level skipif, matching the repo's other
node-driven tests.
2026-06-24 11:07:54 +00:00
nesquena-hermes eb331b5163 release: stamp v0.51.625 (#4813 cron model @provider prefix strip) 2026-06-24 11:02:44 +00:00
nesquena-hermes 984fd9557c Merge remote-tracking branch 'origin/master' into gate/4813-cron-prefix 2026-06-24 11:01:50 +00:00
nesquena-hermes d755ed1c06 test(#4813): cover _cronModelBareName provider-prefix strip matrix
The contributor PR had no test for the new helper. Adds Node-driven coverage:
matching-provider prefix stripped, unprefixed untouched, no/mismatched provider
untouched, empty/null passthrough. Co-authored-by: luandnh
2026-06-24 10:51:24 +00:00
nesquena-hermes 9493f1640f Merge branch 'peek-4813' into gate/4813-cron-prefix 2026-06-24 10:49:48 +00:00
nesquena-hermes b9e4e24cbc release: stamp v0.51.624 (#4832 bound kanban column card-list height) 2026-06-24 10:49:28 +00:00
nesquena-hermes 631f1a35da Merge branch 'peek-4832' into gate/4832-kanban-height 2026-06-24 10:38:49 +00:00
nesquena-hermes 718cefc264 release: stamp v0.51.623 (#4702 iOS portrait open-scroll-to-bottom) 2026-06-24 10:25:20 +00:00
nesquena-hermes 7fe6861a54 test(#4702): thread _lastMessageClientHeight through the scroll-pin-reentry JS harness
The #4295 harness runs the extracted scroll-handler body in a Node new Function();
the body now references _lastMessageClientHeight, so it must be a param + threaded
through state (seeded null, samples keep clientHeight stable so grew stays false and
the pin-reentry assertions are unchanged).
2026-06-24 10:15:04 +00:00
nesquena-hermes 671f4504ae fix(#4702): co-seed _lastMessageClientHeight with every programmatic _lastScrollTop write (Codex gate)
Codex found the guard had a first-event gap: programmatic seeds of _lastScrollTop
(_setMessageScrollToBottom, scrollToBottom, snapshot restore paths) didn't seed
_lastMessageClientHeight, so the FIRST native toolbar-collapse scroll saw a null
prior height -> grew=false -> the false-unpin still fired. Now every programmatic
_lastScrollTop=el.scrollTop is paired with _lastMessageClientHeight=el.clientHeight.
2026-06-24 10:02:52 +00:00
nesquena-hermes fe40912eb9 fix(#4702): don't false-unpin a freshly-opened session on iOS portrait toolbar settle
iOS Safari portrait resolves its dynamic toolbar height after first paint; when the
toolbar collapses the #messages scroller grows (clientHeight up), firing a scroll
event with decreased scrollTop that was misread as an upward user scroll -> falsely
set _messageUserUnpinned, and the ResizeObserver settle then self-cancelled, leaving
the reader stranded at the TOP of a freshly-opened conversation (landscape unaffected
since it doesn't resize late). Sibling of #4701.

Fix (touch-safe, no-op on desktop/landscape where clientHeight is stable):
- Track _lastMessageClientHeight; gate movedUp on !grew so a container-height
  INCREASE can't be read as an upward scroll. Reset on session switch.
- For explicit (open) settles, also observe the scroller itself so a late portrait
  toolbar collapse re-anchors the bottom.

Independent of #4818 (overflow-anchor paint-gap) — different mechanism, coexists.
Diagnosis credit: investigation. Co-authored-by: reinocheong <reporter>
2026-06-24 09:56:06 +00:00
nesquena-hermes 77b59f7c85 test(#4768): drop unused 're' import (ruff F401) 2026-06-24 09:29:15 +00:00
nesquena-hermes 054e39151b release: stamp v0.51.622 (#4768 cron-absent Tasks tab graceful degrade) 2026-06-24 09:24:54 +00:00
nesquena-hermes f4e150e105 Merge remote-tracking branch 'origin/master' into fix/4768-cron-graceful-degrade 2026-06-24 09:23:37 +00:00
nesquena-hermes 1e3132376b fix(#4768): only treat absent cron package as unavailable, re-raise real import bugs (Codex gate)
ModuleNotFoundError fires both when cron.jobs is absent AND when an existing
cron/jobs.py fails to import its own dependency. Catch as exc and only return the
graceful {jobs:[],cron_unavailable:true} when exc.name is 'cron'/'cron.jobs';
re-raise otherwise so a real cron bug isn't silently swallowed.
2026-06-24 09:12:46 +00:00
nesquena-hermes 61f3c766df release: stamp v0.51.621 (#4241 Vietnamese UI language) 2026-06-24 09:11:06 +00:00
nesquena-hermes 1be24e0660 test(#4241): bump remaining hardcoded locale-count assertions for vi (14th locale)
Full-suite caught 7 more per-locale count tests: open_in_vscode (->13, a
pre-existing one-locale gap unrelated to vi), usage_cache_hit_detail/usage_cached_percent
(->14), and the i18n_tooltips LOCALE_COUNT (13->14). vi carries these keys.
2026-06-24 08:50:52 +00:00
nesquena-hermes 34694a1f02 fix(#4768): degrade GET /api/crons gracefully when the cron module is absent
In split-container / minimal Docker deployments the WebUI image may not ship the
agent's cron package on its import path, so the Tasks tab's GET /api/crons did
'from cron.jobs import list_jobs' and 500'd with ModuleNotFoundError — breaking the
whole tab. Now the import is guarded and returns {jobs: [], cron_unavailable: true}
instead, so the Tasks tab loads (empty) instead of erroring. Happy path unchanged.
Co-authored-by: reporter
2026-06-24 08:42:50 +00:00
nesquena-hermes 1fc24ddf68 feat(#4241): add Vietnamese (vi) as 14th UI locale + wire parity
Maintainer-completed @thanhtantran's #4241 (Nathan approved bringing it in):
- vi locale block in static/i18n.js (contributor's 1615-line translation) rebased onto master.
- Added vi to _LOGIN_LOCALE in api/routes.py (login-page localization, issue #1442 parity).
- Bumped the per-locale hardcoded-count assertions 13->14 across the locale-parity
  tests (rtl, quota_chip, hide_suggestions, virtualize_transcript, no_git, help_tab,
  verdigris/zeus skins, auxiliary_models) and added 'vi' to the composer-voice-buttons
  LOCALES tuples. vi already carries all current English keys (verified 14x per key).
All 497 locale/parity tests pass. Co-authored-by: thanhtantran
2026-06-24 08:39:14 +00:00
nesquena-hermes 3de15e53a5 Merge branch 'peek-4241' into gate/4241-vietnamese 2026-06-24 08:26:00 +00:00
nesquena-hermes c3d2fe4a20 release: stamp v0.51.620 (#4808 streaming /api/sessions cache TTL hotfix) 2026-06-24 08:19:43 +00:00
nesquena-hermes 1fc86a47c7 Merge remote-tracking branch 'origin/master' into fix/4808-streaming-sessions-cache-ttl 2026-06-24 08:18:44 +00:00
nesquena-hermes a0c295d4d8 release: stamp v0.51.619 (#4827 reasoning-echo suppression) 2026-06-24 08:09:17 +00:00
nesquena-hermes 8eed95a25e fix(#4808): widen /api/sessions cache TTL while streaming to stop per-poll rebuilds
The frontend polls /api/sessions every 5s while a turn streams
(static/sessions.js _streamingPollMs=5000), but the cache TTL was 2.5s
(_SESSIONS_CACHE_TTL_SECONDS) — so every streaming poll landed in a fresh window
and forced a full all_sessions() rebuild on the hot path under the global store
LOCK, pinning CPU to 100% and starving token rendering on large stores
(recurrence of #4672). The #4680 freeze-marker only de-dupes rebuilds WITHIN one
TTL window; with polls spaced wider than the TTL it never helped.

Fix: while _session_list_cache_streaming_freeze_marker() indicates an active
stream, use _SESSIONS_CACHE_STREAMING_TTL_SECONDS=10.0 (> the 5s poll) so the
sidebar holds steady across streaming polls. Live runtime state (active stream,
sort order, pending flags) is overlaid on every response regardless of cache, and
structural/settings changes still evict immediately via the source stamp — only a
streaming session's own persisted title/message_count can lag up to 10s, which the
#4672 design already deems acceptable. Idle path byte-for-byte unchanged.

Diagnosis credit: investigation reproduced 6 streaming polls -> 6 rebuilds before,
1 after. Co-authored-by: batkuip <reporter>
2026-06-24 08:06:45 +00:00
nesquena-hermes 22b98e3922 Merge branch 'peek-4828' into gate/4828-reasoning-echo
# Conflicts:
#	CHANGELOG.md
2026-06-24 07:57:31 +00:00
nesquena-hermes a51b06910f release: stamp v0.51.618 (#4809 worklog duration preserve) 2026-06-24 07:50:06 +00:00
nesquena-hermes dcce509e7b Merge remote-tracking branch 'origin/master' into gate/4809-worklog-dur
# Conflicts:
#	CHANGELOG.md
2026-06-24 07:49:32 +00:00
nesquena-hermes f3086f4974 Merge branch 'peek-4809' into gate/4809-worklog-dur
# Conflicts:
#	CHANGELOG.md
2026-06-24 07:25:32 +00:00
nesquena-hermes 19af32f1fa release: stamp v0.51.617 (#4818 mobile scroll-jank fix) 2026-06-24 07:23:29 +00:00
nesquena-hermes 92b3ca7046 Merge branch 'peek-4818' into gate/4818-mobile-scroll 2026-06-24 06:59:17 +00:00
nesquena-hermes 9883cf80a0 release: stamp v0.51.616 (#4793 recycle hardening) 2026-06-24 06:50:31 +00:00
nesquena-hermes 988acdb441 Merge remote-tracking branch 'origin/master' into gate/4803-recycle-harden 2026-06-24 06:49:41 +00:00