Commit Graph
100 Commits
Author SHA1 Message Date
nesquena-hermes 764a842785 Release exp: ROADMAP.md accuracy refresh (#5709) 2026-07-11 08:16:22 +00:00
nesquena-hermes 238d1b9aed Merge #5709: full accuracy refresh of ROADMAP.md 2026-07-11 08:15:35 +00:00
nesquena-hermes 43f15d963f Release exp: context-selection counter reset (#5929). Thanks @akay64. 2026-07-11 07:53:44 +00:00
nesquena-hermes 28d81ab40c Merge #5929: reset context-selection counter on clear/session-switch 2026-07-11 07:53:18 +00:00
nesquena-hermesandakay64 e0dea16940 test: cover context-selection counter reset on clear/session-switch (#5929)
Adds regression coverage for the akay64 fix: _selectionIdCounter must reset to 0
when the last chip is removed (_removeNamedContextBlock) and on
_clearPendingSelections, so context chips restart at 'Context 1' after a
clear-all or session switch instead of continuing at 'Context N+1'.

Co-authored-by: akay64 <akay64@users.noreply.github.com>
2026-07-11 07:47:25 +00:00
nesquena-hermes e10f2b1eea Release exp: redaction-awareness prompt guidance (#5927). Thanks @mvanhorn. 2026-07-11 07:24:10 +00:00
nesquena-hermes adbe272cc9 Merge #5927: teach ephemeral prompt that redacted credentials are intentional masking 2026-07-11 07:23:43 +00:00
nesquena-hermes 1ab1e8d3e1 Release exp: stale sidebar unread-dot on visit (#5917). Thanks @neaucode-bot. 2026-07-11 06:21:30 +00:00
nesquena-hermes 45fe6b4f10 Merge #5917: clear stale sidebar unread dot on session visit 2026-07-11 06:20:50 +00:00
nesquena-hermes 90e8f368fd test: widen fixed-window assertions after busy-reset reorder
The #5917 reorder (idle busy/stream reset moved before the visit-ack) shifted
code positions, so two pre-existing fixed-offset source-assertion tests
truncated: extend gateway_sse_reconnect_dedupe's loadSession _block end-anchor to
_mergePendingSessionMessage, and widen stale_stream_cleanup's 900->1400 char
window past the expanded comment. Behavior unchanged.
2026-07-11 06:06:04 +00:00
nesquena-hermes cb94cecc1a fix(sessions): reset busy/stream flags before the visit-ack repaint (gate)
Gate finding: the idle-session S.busy/S.activeStreamId reset ran AFTER
_acknowledgeSessionVisit(), so the ack's sidebar repaint could inherit the
PREVIOUS session's busy state and manufacture a phantom unread/streaming
indicator on the freshly-loaded idle session. Moved the activeStreamId calc +
idle reset BEFORE the acknowledge call. Co-authored-by: neaucode-bot.
2026-07-11 05:52:56 +00:00
nesquena-hermesandneaucode-bot 05f6e8c7f4 fix(sessions): guard viewed-count clear in message-load + derive stream state from target session (gate findings)
Co-authored-by: neaucode-bot <neaucode-bot@users.noreply.github.com>
2026-07-11 05:39:19 +00:00
nesquena-hermes 6622ab1676 Release exp: batch salvage-reliability #5909 + #5912 + #5915
Thanks @alexfoxtm, @harryazj, @brianmmaina.
2026-07-11 05:32:30 +00:00
nesquena-hermes b33c008de2 Merge salvage/5294-mobile-dictation 2026-07-11 05:27:11 +00:00
nesquena-hermes 42fcdb0090 Merge salvage/4750-composer-clear 2026-07-11 05:27:11 +00:00
nesquena-hermes 7f88b66ff9 Merge salvage/5461-plugin-catalog 2026-07-11 05:27:11 +00:00
nesquena-hermes 7176b3a756 fix(config): route plugin providers before config_provider bare-passthrough
Gate found the plugin-routing branch ran AFTER the provider==config_provider
early-return, so when an ACTIVE plugin provider was also the configured default,
model_with_provider_context returned a bare model and dropped the '@plugin:'
hint -> wrong backend. Moved the _is_plugin_model_provider() branch above the
config_provider passthrough. Regression test (GMI + anthropic/claude-sonnet-4.6
as the active plugin+configured provider) added. Co-authored-by: alexfoxtm.
2026-07-11 05:18:27 +00:00
nesquena-hermes 63d3efb6c4 fix(sessions): guard post-load unread re-ack on active view (gate finding)
Gate found the unconditional post-load _acknowledgeSessionVisit() cleared a
hidden-tab completion's unread marker: a completion landing while
_ensureMessagesLoaded() is in flight AND the tab then goes hidden is correctly
marked unread, but the final re-ack wiped it. Gate the re-ack on
_isSessionActivelyViewedForList(sid) so only an actively-viewed session clears.
Regression test added. Co-authored-by: neaucode-bot.
2026-07-11 05:15:01 +00:00
nesquena-hermes 4d3b66976c Release exp: max reasoning level, availability-gated (#5910). Thanks @perejaslav. 2026-07-11 04:48:26 +00:00
nesquena-hermes bd1cdf24c7 Merge #5910: add 'max' reasoning effort level (opt-in, availability-gated) 2026-07-11 04:47:37 +00:00
nesquena-hermesandperejaslav 742ccd59b5 feat(reasoning): make 'max' selectable only when actually available (#3505 refinement)
Maintainer call (2026-07-11): 'set max when available, don't when not.' Two layers:
- resolve_model_reasoning_efforts() now uniformly passes its sourced list through
  the provider ceiling filter (preserving any 'none' sentinel), so the UI dropdown
  offers 'max' ONLY for models whose native ladder genuinely includes it (adaptive
  Claude 4.6+, DeepSeek) and hides it everywhere it would be rejected/mishandled
  (GPT-5, o-series, Gemini, legacy/cloud-hosted Claude, unknown providers).
- coerce_reasoning_effort_for_model() default-denies a stale/CLI 'max' to xhigh on
  an UNRECOGNIZED provider (empty capability list) so it can never 400, while a
  RECOGNIZED reasoning provider whose exact model id we couldn't resolve (e.g.
  claude-opus-latest) still preserves 'max'. All other levels keep #3505
  preserve-verbatim. New _provider_known_reasoning_capable helper + regression tests.

Co-authored-by: perejaslav <perejaslav@users.noreply.github.com>
2026-07-11 04:42:35 +00:00
nesquena-hermes 5d2740cf56 fix(send): clearPending:false + preserve file objects (gate round 2)
Re-gate found 2 issues in the prior fix: (1) [CORE] uploadPendingFiles still
defaulted clearPending=true and re-cleared S.pendingFiles after the await,
wiping any attachment staged during the upload window — pass clearPending:false
since we already detached the snapshot. (2) [SILENT] converting File objects to
name strings broke draft-restore signature matching ({value:name} vs canonical
{name,path,size,type}) — preserve the file objects with [..._submittedFiles].
2026-07-11 04:13:53 +00:00
nesquena-hermes 1e5f86f190 Release exp: opt-in start-new-chat-on-workspace-switch (#5473). Thanks @ruizanthony. 2026-07-11 04:11:16 +00:00
nesquena-hermes 45d6646007 Merge #5473: opt-in start-new-chat-on-workspace-switch 2026-07-11 04:10:39 +00:00
nesquena-hermes b779f0b777 fix(reasoning): extend max ceiling to OpenAI-family + cloud-hosted Claude lanes
Re-gate found 'max' still reached provider adapters that reject/mis-handle it on
lanes beyond openai-codex/native: direct openai + openai-api + azure-foundry
GPT-5 (APIs top out at xhigh), and Azure-Foundry/Bedrock-hosted LEGACY Claude
(the pre-adaptive classifier only matched native Anthropic provider names, so max
survived to the 8k manual-thinking budget). Extended the GPT-5/o-series ceiling
to all OpenAI-family lanes and the pre-adaptive-Claude ceiling to azure/bedrock/
vertex lanes (gated on 'claude' in the model id so non-Claude models on those
lanes are untouched). Regression tests for both lanes.
2026-07-11 04:06:36 +00:00
nesquena-hermes d6323bfff0 fix(send): snapshot files + clear draft pre-await (gate fixes)
#5912 gate found 2 defects the capture-clear salvage exposed:
- [CORE] uploadPendingFiles() read still-live S.pendingFiles, so a re-entrant/
  interrupt send during the upload window inherited + re-uploaded the first
  send's attachment. Now snapshot _submittedFiles before the await, detach from
  S.pendingFiles, and upload exactly that snapshot.
- [SILENT] the persisted-draft clear ran AFTER the await, cancelling a draft
  typed during the upload window. Moved it (and _composerDraftClearPromise)
  alongside the textarea clear, before any await. Failed-send restore still
  chains off the promise via the pre-await _failedSendFilesSnapshot.
Updated source-assertion tests for the new call shape + ordering.
2026-07-11 03:58:52 +00:00
nesquena-hermes 381bcb4822 fix(reasoning): classify date-stamped + claude-3 IDs as pre-adaptive
Re-gate found _is_pre_adaptive_anthropic misclassified date-stamped legacy IDs
(claude-sonnet-4-20250514, claude-opus-4-20250514) and the claude-3 family
(claude-3-opus-20240229) as adaptive, so 'max' survived to the 8k manual-thinking
fallback instead of degrading to xhigh (32k-equivalent). Rewrote the helper:
claude-3* always pre-adaptive; a 6+ digit date stamp is not a minor version so
'4-20250514' is a pre-adaptive 4.0 build; 4.6+/latest stay adaptive. Extended
regression tests to both ID shapes.
2026-07-11 03:52:10 +00:00
nesquena-hermesandperejaslav 96b04359ab fix(reasoning): degrade 'max' via provider ceiling, not sourced list
Gate found removing the max->xhigh coercion let 'max' reach provider adapters
whose native ladder tops out lower, degrading WORSE than before: Gemini treats
unknown 'max' as medium; pre-adaptive Claude (3.7/4.5) manual-thinking lacks a
'max' budget and falls to 8k. Fix: coerce_reasoning_effort_for_model now applies
the hard provider ceiling (_filter_reasoning_efforts_for_provider) FIRST and
degrades down the ladder when the ceiling excludes the requested level — even
when the sourced capability list is empty (unrecognized) or wrongly advertises
the level. Gemini + all Google aliases and pre-4.6 Anthropic now cap 'max' at
xhigh; adaptive Claude 4.6+/DeepSeek/etc. preserve it. Regression tests added.

Co-authored-by: perejaslav <perejaslav@users.noreply.github.com>
2026-07-11 03:40:11 +00:00
nesquena-hermesandneaucode-bot 58af505cc6 fix(sessions): clear stale sidebar unread indicator on session visit (salvage of #4946)
Salvaged from #4946 by @neaucode-bot. Rebuilt fresh on master (repro-confirmed),
without regressing hidden-tab completion state or lineage-parent unread dots.

Co-authored-by: neaucode-bot <neaucode-bot@users.noreply.github.com>
2026-07-11 03:32:26 +00:00
nesquena-hermesandruizanthony 54efd6078e feat(workspace): opt-in 'start new chat when switching workspace' (#5473)
Salvage-rebuild of #5473 (@ruizanthony), reworked to OPT-IN per maintainer
decision. New default-OFF setting new_chat_on_workspace_switch: when enabled,
selecting a DIFFERENT workspace starts a fresh chat bound to that workspace
(leaving the current conversation on its original workspace, avoiding stale
cross-workspace context) instead of mutating the current session in place.
Same-workspace selection stays an in-place refresh. Default off preserves the
shipped behavior for everyone.

Wiring: config default + bool key; window._newChatOnWorkspaceSwitch set on
boot + autosave; Settings checkbox (Preferences); gated branch in
switchToWorkspace() that runs before the in-place /api/session/update; 3 i18n
keys across all 15 locales; regression tests (default-off, gated-branch-present,
flag-wiring, checkbox+i18n).

Co-authored-by: ruizanthony <ruizanthony@users.noreply.github.com>
2026-07-11 03:13:48 +00:00
nesquena-hermesandbrianmmaina 18845fd000 fix(#4732): keep mobile composer-mic dictation active through pauses (salvage of #5294)
Salvaged from #5294 by @brianmmaina. Rebuilt fresh on master with the maintainer gate fix-spec: continuous+restart gated to MOBILE (desktop stays one-shot), wake-lock async race serialized, error-path toasts + abort-loop bound.

Co-authored-by: brianmmaina <brianmmaina@users.noreply.github.com>
2026-07-11 03:05:43 +00:00
nesquena-hermes 8c9d3635e2 lint: fix B904 raise-from in share session-resolve + drop unused importlib
CI ruff gate flagged 3 new violations: two api/routes.py KeyError re-raises in an
except-KeyError block (add 'from None') + an unused importlib import in the share
security test. All trivial.
2026-07-11 02:42:59 +00:00
nesquena-hermes 55509e98c2 i18n(cs): add share_session/stop_sharing keys for Czech locale parity
Czech (#5546) was added after #5337 authored its share i18n keys, so the
strict cs-parity test failed on the 14 missing share_session/stop_sharing keys.
Added them to the cs block (English fallback values per locale convention).
2026-07-11 02:37:57 +00:00
nesquena-hermes 82d3bfd51a Release exp: public read-only conversation sharing (#5337). Thanks @MicoRobot. 2026-07-11 02:22:13 +00:00
nesquena-hermes 455ab12dc1 Merge #5337: public read-only conversation sharing 2026-07-11 02:21:41 +00:00
nesquena-hermes a1312acc5d fix(share): reject dict-valued text-blocks and dict titles (re-gate edge)
The re-gate found the always-on sanitizer still had two str()-on-dict paths:
a list text-block whose 'text' is itself a dict (via /api/session/import), and
a dict-valued session title. Both are now type-checked to str; a dict title
falls back to 'Untitled'. Regression cases added. Co-authored-by: MicoRobot.
2026-07-11 02:14:07 +00:00
nesquena-hermesandMicoRobot. 75153f5b79 fix(share): always-on redaction boundary + rebase/UX fixes (gate findings)
Adversarial security re-gate on the rebased head found 4 issues; fixed:

1. [CORE] api/shares.py public snapshot could leak credentials/paths/tool
   payloads. The sanitizer relied on redact_session_data (gated on the
   user-toggleable api_redact_enabled) — disabling it exposed API keys, a dict
   assistant content was stringified as raw tool data, and session.workspace
   survived in transcript text. Now the per-message sanitizer applies ALWAYS-ON
   credential redaction (_redact_fn_cached, force=True, ignores the setting) +
   scrubs known session/workspace/home paths, and refuses to stringify non-text
   structured content. Title is force-redacted too. Regression test proves the
   boundary holds with api_redact_enabled=False.
2. [SILENT] api/models.py: my rebase wrongly kept anchor_activity_scenes in
   METADATA_FIELDS, moving 250-480KB scene bodies before messages and undoing
   master's metadata fast-path (#5854). Removed; 17 #5854 tests pass again.
3. [SILENT] static/index.html: the Share/StopSharing/ExportHTML button triplet
   was double-applied by the rebase (duplicate IDs → only the first wired).
   Deduped to one each + a count assertion test.
4. tests: fixed the stale Session.load-after-/api/session/new setup (master
   keeps new sessions memory-only until first message).

Also (UX, found in the visual flow sweep): a clipboard-copy failure after a
SUCCESSFUL share showed a red 'Share failed' toast and skipped opening the page.
The copy is now isolated — the share succeeds, the page opens, and the toast
shows the link if the clipboard write fails, never a false failure.

Co-authored-by: MicoRobot.
2026-07-11 02:06:34 +00:00
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-hermesandharryazj 67c9277ed4 fix(send): clear composer immediately after capture to prevent stalled-text race (salvage of #4750)
Salvaged from #4750 by @harryazj — pulling ONLY the atomic composer capture-and-clear fix from the larger PR. Prevents a re-entrant/interrupt-mode send from re-reading stale textarea DOM and double-sending.

Co-authored-by: harryazj <harryazj@users.noreply.github.com>
2026-07-11 01:39:19 +00:00
nesquena-hermesandalexfoxtm 553ad8146e fix(api/config): surface + route plugin-only providers in static catalog
Salvaged from #5461 by @alexfoxtm — the catalog-surfacing half was correct; this rebuilds it fresh on master and adds the missing routing (model_with_provider_context -> @plugin:model) + a routing regression test.

Co-authored-by: alexfoxtm <alexfoxtm@users.noreply.github.com>
2026-07-11 01:11:37 +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