Commit graph

5521 commits

Author SHA1 Message Date
nesquena-hermes
9b80e9b06f
Merge pull request #5074 from nesquena/stage/skins-batch
Release: 3 new opt-in skins (GitHub, Codex, Terracotta)
2026-06-27 17:01:43 -07:00
nesquena-hermes
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-hermes
e768053e4a
Merge pull request #5073 from nesquena/stage/4958-anchor-mixed-order
Release: preserve settled mixed content order (#4958)
2026-06-27 16:21:24 -07:00
nesquena-hermes
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
5cd070ae76
Merge pull request #5072 from nesquena/stage/4986-compress-durability
Release: make manual /compress durable across reload + restart (#4986)
2026-06-27 14:53:26 -07:00
nesquena-hermes
919901265f test(#4986): drop unused imports (ruff F401) 2026-06-27 21:49:23 +00:00
nesquena-hermes
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-hermes
6ecb231451
Merge pull request #5071 from nesquena/stage/5069-boot-composer
Release: preserve typed composer input during boot restore (#5069)
2026-06-27 13:56:01 -07:00
nesquena-hermes
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-hermes
3fa1c0a324
Merge pull request #5068 from nesquena/stage/4936-dashboard-chip
Release: sync a Hermes Dashboard chip with Appearance dropdown (#4936)
2026-06-27 13:37:50 -07:00
nesquena-hermes
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-hermes
aa6eb35de7
Merge pull request #5067 from nesquena/stage/5010-max-tokens
Release: expose max_tokens override in Settings (#5010)
2026-06-27 13:32:40 -07:00
nesquena-hermes
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-hermes
93a7b96fbc
Merge pull request #5066 from nesquena/stage/5065-gateway-health
Release: fix cross-container gateway health (#5065)
2026-06-27 12:59:13 -07:00
nesquena-hermes
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-hermes
16a6827f73
Merge pull request #5063 from nesquena/stage/4978-oob-strip
Release: strip consumed OOB steer blocks from model-facing history (#4978)
2026-06-27 12:17:24 -07:00
nesquena-hermes
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-hermes
e848acc236
Merge pull request #5062 from nesquena/stage/5061-metadata-refresh
Release: reload active transcript only on message-count change (#5061)
2026-06-27 11:53:28 -07:00
nesquena-hermes
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-hermes
6ba4194d18
Merge pull request #5059 from nesquena/stage/4647-cancel-preserve
Release: preserve cancelled partial turn payloads (#4647)
2026-06-27 09:53:36 -07:00
nesquena-hermes
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
d968c741f9
Merge pull request #5056 from nesquena/release/stage-5053-selfheal
Release: read-side sidecar self-heal from newer state.db, lock-safe (#5053)
2026-06-27 08:38:08 -07: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
allenliang2022
363097adad fix(webui): hold per-session lock for state.db sidecar self-heal (TOCTOU + clobber) 2026-06-27 15:24:15 +00:00
allenliang2022
dbe470aa6f test(webui): add registration-window race regression + grace guard 2026-06-27 15:24:15 +00:00
allenliang2022
f3da26dc07 fix(webui): address greptile P1 - snapshot-first save, safer recovery gate 2026-06-27 15:24:15 +00:00
allenliang2022
a3030fb498 fix: sync stale sidecars from newer state db 2026-06-27 15:24:15 +00:00
nesquena-hermes
4584068b7a
Merge pull request #5054 from nesquena/release/stage-5051-coalesce
Release: coalesce session-visit model refreshes, bounded + hang-safe (#5051, fixes #5047)
2026-06-27 06:49:42 -07: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
Rod Boev
fca1d4bf2b chore(#5047): retrigger unrelated red shard 2026-06-27 13:35:24 +00:00
Rod Boev
9b9ff4053f fix(#5047): honor bounded refresh follower latency 2026-06-27 13:35:24 +00:00
Rod Boev
6649db7041 fix(#5047): prevent sync refresh follower hangs 2026-06-27 13:35:24 +00:00
Rod Boev
4f2bcada31 fix(#5047): keep sync refresh followers coalesced 2026-06-27 13:35:24 +00:00
Rod Boev
8da4c7a334 fix(#5047): require live rebuild publication for refresh reuse 2026-06-27 13:35:24 +00:00
Rod Boev
bcfec68c5c fix(#5047): cover bounded refresh followers 2026-06-27 13:35:24 +00:00
Rod Boev
53d44d3795 fix(#5047): coalesce session-visit refresh followers 2026-06-27 13:35:24 +00:00
nesquena-hermes
db15233c4f
Merge pull request #5052 from nesquena/release/stage-5050-memhover
Release: memory-section hover path titles (#5050, fixes #5045)
2026-06-27 03:57:19 -07:00
nesquena-hermes
e7cd8f5f12 Release: memory-section hover path titles (#5050, fixes #5045) 2026-06-27 10:52:29 +00:00
Rod Boev
f65543f88e fix(#5045): add memory-section hover path titles 2026-06-27 06:05:28 -04:00
nesquena-hermes
8b2d3db2e6
Merge pull request #5044 from nesquena/release/stage-5041-approval
Release: relay mirrored gateway approvals after stream loss (#5041, fixes #5000)
2026-06-26 23:19:25 -07:00
nesquena-hermes
e47c336981 Release: relay mirrored gateway approvals after stream loss (#5041, fixes #5000) 2026-06-27 06:14:26 +00:00
Rod Boev
e621d7360a test(#5000): keep gateway approval regressions CI-portable 2026-06-27 06:03:24 +00:00
Rod Boev
8f0cc7912b fix(#5000): settle mirrored gateway approval cleanup 2026-06-27 06:03:24 +00:00
Rod Boev
5f1e308341 fix(#5000): relay mirrored gateway approvals after stream loss 2026-06-27 06:03:24 +00:00
nesquena-hermes
2d9422adf2
Merge pull request #5043 from nesquena/release/stage-4964-postinstall
Release: extension post-install next steps in gallery (#4964, fixes #4959)
2026-06-26 22:58:33 -07:00
nesquena-hermes
470769454f Release: extension post-install next steps in gallery (#4964, fixes #4959) 2026-06-27 05:53:40 +00:00
nesquena-hermes
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
1b3b49b33d
Merge pull request #5042 from nesquena/release/stage-4979-extruntime
Release: extension sidecar runtime status in diagnostics panel (#4979)
2026-06-26 22:37:35 -07:00
nesquena-hermes
0825612a59 Release: extension sidecar runtime status in diagnostics panel (#4979) 2026-06-27 05:33:40 +00:00
nesquena-hermes
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