Merge upstream v2026.8.3 — Hermes v0.20.0 catch-up - #352
Merged
Conversation
Right-click any tab and pick Reload: the pane's content remounts in place — effects re-run, state resets, measurements are retaken — while the tab keeps its slot and every other tab is untouched. A per-pane epoch atom keys the contribution inside the zone body, so reload never rewrites the layout tree. Both tab menus offer it: the zone strip menu (tool panels, the file tree, a fresh draft's main tab) and the session tab menu (tiles + the loaded main tab).
Reload a tab from its right-click menu
…licit denials
When an approval prompt expired without a response, every CLI-side path
collapsed the timeout into the same 'deny' choice as an explicit user
refusal, so the agent was told the user denied the action when the user
simply never answered. The gateway wait already distinguished the two
('timed out without user response... Silence is not consent.'); this
brings the CLI/TUI/ACP surfaces to parity.
- prompt_dangerous_approval(): input()-path expiry now returns a distinct
'timeout' choice (still fail-closed).
- cli.py _approval_callback + hermes_cli/callbacks.py approval_callback:
deadline expiry returns 'timeout' instead of 'deny'.
- check_all_command_guards / _run_approval_gate CLI tails: 'timeout' maps
to outcome='timeout' with a 'timed out without user response... Silence
is not consent.' BLOCKED message (matching the gateway wording);
explicit deny keeps outcome='denied' and gains user_consent=False for
shape parity.
- computer_use: 'timeout' verdict threads through the CLI adapter and
yields a 'prompt timed out — the user did not respond' error instead of
'denied by user'.
- ACP permissions bridge: FutureTimeout returns 'timeout' (other failures
still 'deny'); elicitation maps 'timeout' to 'cancel' like the gateway's
unresolved outcome; codex wire mapping documents deny/timeout→decline.
- write_approval already treats unknown choices as 'stage, not drop', so
a timeout now stages the memory write instead of silently refusing it.
Every timeout path remains fail-closed — the action never runs; only the
classification reported to the agent changes.
Copying a Discord thread (or any rich-text selection with images) attached one or more blank thumbnails and dropped the message text entirely. Two causes. The clipboard's `text/html` was scraped for inline `<img src="data:…">` regardless of whether the copy carried its own text — and what Discord ships beside each image embed is a 32x5 blurhash placeholder, which is exactly the blank attachment. Then, because any image blob short-circuited the paste handler, the prose that came with it never reached the composer. Inline HTML images now only count for an image-only copy, and are ignored below a thumbnail-sized floor so spacers and trackers don't attach either. A mixed paste attaches its real images and still inserts its text. Also registers pasteAndMatchStyle in the Edit menu — Cmd+Shift+V had no menu entry, so the chord was never translated into an editor command anywhere in the app.
A session's YOLO bypass lived only in the in-memory tools.approval._session_yolo set (or the process-frozen --yolo env var), so resuming a session in a fresh process silently reverted the user's /yolo ON — dangerous commands started prompting again. Persist a yolo_mode flag in the session row's model_config JSON and restore it on every CLI resume path: - SessionDB.set_session_yolo() merges the flag into model_config (same lineage-preserving merge as update_session_runtime_lock); SessionDB.session_yolo_enabled() reads it back, false on any parse failure. - /yolo toggle persists ON and OFF through the new helper; the compression/branch session-id rotation carries the flag onto the continuation row. - --yolo launches record the flag at session creation (agent_init), and a /yolo toggled before the lazily-created row exists is carried into the creation-time model_config (_ensure_db_session). - HermesCLI._restore_session_yolo() re-enables the bypass on startup --resume/-c, the deferred init path, and mid-chat /resume, with a visible '⚡ YOLO mode restored from session' notice. No-op under a frozen process-wide --yolo and never enables on absent/garbage flags.
…ixed-paste Pasting a thread with images keeps the text and skips the blank thumbnails
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
…-egilewski-myk0la chore: contributor email mappings for egilewski and myk0la-b
…-ckaznocha chore: AUTHOR_MAP for ckaznocha
…search#76870) Deferred model switches append a marker and bump history_version at turn start; the dispatcher was snapshotting history before that mutation, so the version-mismatch guard rejected the turn's own result as a stale/concurrent write. Move the snapshot to after _apply_pending_model_switch/_sync_agent_model_with_config, under history_lock, so the turn's own preparatory mutation is included in its baseline while the anti-stale guard still catches real external writes. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
teknium1 flagged ISSUE_76870_RELATORIO_CAUSA_RAIZ.md as containing stale metadata (references an unrelated local branch) and asked to drop the standalone report, keeping only the focused server.py fix and regression test.
…#33208) strip_think_blocks passed the same response-scrubbing strings through re's pattern dispatcher on every response. Skills Guard repeated the same work for 121 patterns against every scanned line. Compile the existing expressions once and reuse Pattern.sub/search. Keep each generic tool-call tag in its own paired expression so mismatched openers retain their payload while existing stray-closer cleanup remains unchanged. Part of NousResearch#33208 Salvaged from NousResearch#32713 by @ErnestHysa. Co-authored-by: ErnestHysa <takis312@hotmail.com>
Simplify-pass follow-up on the NousResearch#69653 salvage: the original code built these patterns from a name loop; hand-expanding them into 10 literals lost that single source. Tag-name tuples restore it (adding a 6th reasoning tag is now a one-place change), and the gnarly named-function pattern regained a pointer to its step-1c rationale. Byte-equivalence of every rebuilt pattern verified programmatically (alternation-order neutrality probed: the \b and > anchors make order irrelevant).
at828@proton.me -> ATran28 (GH id=1445620) Needed for PR NousResearch#77270 whatsapp bridge reconnect fix.
…-map-atran28 chore: add ATran28 to AUTHOR_MAP
…search#77184) request_restart was calling stop() immediately, so the requesting turn stayed in the drain wait set and got force-killed at restart_drain_timeout. Wait for active work to reach zero first, then stop against an idle gateway.
… compute-host shutdown ComputeHost.shutdown() called flush_all_sessions() before its own in-flight turn drain loop. server._finalize_session latches on session["_finalized"] and every later call returns immediately, so that one flush was spent while turns were still producing output: the unflushed tail was never persisted, commit_memory_session wrote long-term memory from a truncated transcript, the session's DB row was marked ended while it was live, on_session_end fired with completed=False/interrupted=True against a running session, and the active-session lease was released out from under a turn. The drain loop exists precisely so that mid-turn work survives a teardown; finalizing first defeated it. Reachable from all three teardown paths: the parent/orphan guard (which os._exit(0)s immediately after), the SIGTERM/SIGINT handler, and stdin close. Drain first, then flush. A slice of the caller's budget (_FLUSH_RESERVE_SECS, never more than half of it so a short explicit wait still gets a real drain) is withheld from the drain so the flush still runs when turns outlast the window: HostSupervisor SIGKILLs the host _SHUTDOWN_TIMEOUT_SECS after SIGTERM — 10.0s, the same value as shutdown()'s default wait — so a drain allowed to consume the whole budget would leave the durability write racing that kill. `wait` itself is unchanged, so total shutdown latency and the SIGTERM->SIGKILL margin are unchanged.
The drain loop slept a flat 0.05s per tick, so it could overshoot its deadline by up to one tick and spend part of the reserve withheld for flush_all_sessions(). For a small `wait` the reserve is itself half the budget, so a single overshoot can consume all of it: at wait=0.34 the drain budget is 0.17s but the loop requested 4 x 0.05 = 0.20s of sleep. Clamp each tick to the remaining time. The new test asserts on the summed *requested* sleep rather than wall-clock, which is deterministic: every sleep is bounded by the strictly-decreasing remainder, so the total can never exceed the drain budget regardless of how the scheduler interleaves.
…n deadline expires The drain reserves a slice of the shutdown budget so flush_all_sessions still runs when in-flight turns outlast the window. But that flush was unconditional: a session whose turn was still running got its one-shot _finalize_session spent mid-turn, and the executor.shutdown(wait=False, cancel_futures=True) immediately after does not join the turn. The session was then permanently un-finalizable and its active-session lease had been released out from under live work — the same persistence and lifecycle race the drain exists to close, just relocated past the deadline instead of removed. Give _turn_futures a session association (Future -> sid, the same key space as server._sessions) at both submit sites, and on deadline expiry exclude the sids whose futures are still running from the flush. Those sessions are retained unfinalized and therefore recoverable; sessions with no live turn finalize exactly as before. The done-callback now pops under the lock, since a bare dict.pop is not the drop-in set.discard was. wait semantics, the reserve math and the bounded per-tick sleep are unchanged, so this adds no shutdown latency. All three shutdown callers (orphan, sigterm, and the tight stdin_closed wait=2.0 path) funnel through this one function and are covered.
The PR's skip-live-sessions optimization is partially defeated by
server._shutdown_sessions() registered via atexit (server.py:1172),
which runs on SystemExit after shutdown() returns for the SIGTERM and
stdin_closed paths. The orphan path (os._exit(0)) bypasses atexit.
This is a pre-existing issue — the old finalize-first order had the
same atexit interaction. The comment documents the gap and suggests a
follow-up: gate _shutdown_sessions on not session.get('running').
…croll jitter (fixes NousResearch#73629) Fix direction inspired by PR NousResearch#73674 by @drbronson with added Vitest component unit tests.
Review follow-up on the NousResearch#75714 salvage: perfectionist/sort-imports would fail lint CI; autofixed.
`StatusRulePane` renders `StatusRule` outside the `!isBlocked` guard in appLayout.tsx, so the status rule stays mounted underneath approval, model-picker, pager, sessions and every other blocking overlay. Its three timer-driven components keep firing the whole time: `FaceTicker` (glyph, 1s clock, verb rotation), `SessionDuration` (1s) and `IdleSince` (1s). Every tick re-renders a rule nobody can see, and in an Ink TUI that churn reads to the user as the dialog flickering. Gate all three components' interval creation on the existing `$isBlocked` computed store, so nothing is armed while an overlay covers the rule. The pause alone would leave the elapsed read-outs frozen at the moment the overlay opened, so each effect re-seeds `now` from the wall clock when it re-arms. `SessionDuration` and `IdleSince` already did this; `FaceTicker` gains the same re-sync. Closing a five-minute overlay now resumes at the true elapsed time instead of the pre-overlay value. No new store is introduced — `$isBlocked` already exists and already ORs the current OverlayState field set.
`$isBlocked` answers "is text input suspended", not "is the status rule covered". appLayout uses it only to hide the input rows (appLayout.tsx:384); `StatusRulePane` renders outside that guard, at :365 for `at="top"` and :449 for `at="bottom"`. So the previous revision paused FaceTicker / SessionDuration / IdleSince under prompts that leave the rule fully on screen — approval, billing, subscription, confirm, clarify, sudo and secret all render through PromptZone in NORMAL FLOW above ComposerPane (appOverlays.tsx:58-162, appLayout.tsx:553-568). They push the rule down; they do not cover it. Freezing a visible clock is a worse bug than the churn being removed. Replace it with `$isStatusRuleOccluded`, a narrow derived store over overlay + ui state covering only what actually paints over the rule: - `widget` — the modal widget slot renders at viewport level (ActiveWidgetSlot, sdk/host.tsx:209) so it can anchor the full-screen absolute `Overlay` against the whole terminal. - the FloatingOverlays set (modelPicker, pager, petPicker, sessions, skillsHub, pluginsHub) — but only when `ui.statusBar === 'top'`. That panel is `position="absolute" bottom="100%"` inside ComposerPane's relative Box (appOverlays.tsx:387), so it grows UPWARD over the top rule and can never reach the bottom one. Deliberately excluded: the PromptZone flow states above; `agents` and `journey`, which unmount the entire ComposerPane subtree (appLayout.tsx:553) so React's effect cleanup already clears the intervals; `ambient`, an in-flow dock; and composer completions, which share the floating grid but are a render prop that changes per keystroke — re-arming a 1s interval on every character would restart the countdown each time and starve the tick. `statusBar: 'off'` needs no branch: StatusRulePane returns null for both slots, so the timers never mount. Tests: the store-level cases are re-split into occluding and non-occluding sets, and an AppLayout-level `describe` mounts the real layout so the rule sits in its true position — asserting that under approval and sudo the rule is still rendered AND its clock advances (1m 0s to 1m 30s), that a floating model picker suppresses the clocks with the rule at the top, and that the same picker leaves them armed with the rule at the bottom.
Review follow-up for salvaged PR NousResearch#76782. Three setup-wizard validation functions called _normalize_openviking_url outside their try/except blocks. Since _normalize_openviking_url now raises _OpenVikingEndpointError for blocked or malformed endpoints, an invalid endpoint would crash the wizard instead of returning a friendly (False, message) tuple. - _validate_openviking_auth: move _normalize_openviking_url inside try - _validate_openviking_root_access: same - _validate_openviking_setup_values: catch _OpenVikingEndpointError explicitly - Remove dead ternary in _normalize_openviking_url safety check (candidate always has http/https scheme by that point) - Replace redundant float('-inf') < x < float('inf') with math.isfinite() in _setting_float; drop the redundant infinity check from _setting_int (is_integer() already rejects inf/nan)
…-cicav chore: contributor email mapping for cicav (legacy noreply form)
…ckup/vendor dirs SubdirectoryHintTracker re-injected identical context files whenever the same AGENTS.md was reachable through more than one path. Symlinked shared workspaces, hardlinks, and timestamped backup copies all alias a single file, so a normal session could ship the same 8KB of instructions two or three times. Nothing deduped it and nothing excluded directories that only ever hold copies. Two changes: * Track a sha256 of every injected hint body. Repeat content is skipped, and the working directory's own context file is seeded at construction so the copy prompt_builder already loaded at startup is never sent again. * Skip directories that hold copies rather than authoritative context (backups, node_modules, venv, site-packages, .git, .Trash, vendor, caches). Screening is relative to working_dir, so a project that legitimately lives under vendor/ keeps discovering its own subdirectory hints. Measured on a real session that touched a symlinked shared workspace: 3 injections / ~24,000 chars before, 1 injection / 8,112 chars after. 14 new tests cover symlink aliasing, byte-identical copies, working-dir seeding, distinct content still being injected, each excluded directory name, excluded ancestors, and the working-dir-inside-excluded-name case.
Re-derivation of NousResearch#23254 (@devsart95) on today's flush loop. The turn flush in _flush_messages_to_session_db wrote one BEGIN IMMEDIATE transaction per message row; a typical agent turn (user + assistant + tool results) paid 3-8 transactions -- and, off WAL (the default on macOS while the WAL-reset guard is active), 3-8 fsyncs -- per turn. Adds SessionDB.append_messages_batch: same row shape as append_message (shared _prepare_message_row serializer + _MESSAGE_INSERT_SQL column list, so the two writers cannot drift), same compression-lock and compression-closed guards, one aggregated session-counter UPDATE, one transaction for the whole batch. Row serialization stays outside the write lock. The flush loop now collects the turn's new rows and writes them in one call. All-or-nothing pairs exactly with the persisted-marker stamping: on failure no rows landed and no markers were stamped, so the next flush re-writes the whole tail (same recovery contract as before, minus the partial-prefix case that could double-count). Measured (same harness, 5-message turn, journal_mode=DELETE, synchronous=FULL): 2.32ms -> 0.83ms median per turn flush (64% faster, 5 fsyncs -> 1). On WAL the win is smaller but the atomicity fix holds.
Sibling sites of the per-message flush pattern: both branch-seed paths (session.branch in methods_session.py and the lazy seed persist in server.py) copied the parent history row-by-row -- one transaction per row, and a branch seed can be hundreds of rows. Route both through SessionDB.append_messages_batch. The server.py path also gains real atomicity: _branch_seed_persisted assumed every row landed, which the per-row loop could not guarantee.
…rites The flush now goes through append_messages_batch; MagicMock-based assertions and barrier fakes that hooked append_message observed nothing (the flush's try/except swallowed the AttributeError). Assert on the batch payload instead.
… share guards, chunk seeds Simplify-pass folds on the NousResearch#23254 salvage: - REUSE (HIGH): append_messages_batch now delegates row serialization to the pre-existing _insert_message_rows helper (already shared by replace_messages / archive_and_compact / portability import) instead of adding a third serialization path (_prepare_message_row + _MESSAGE_INSERT_SQL are gone). One row-writer for every multi-row path; the row-ID return was consumed by no production caller, so the batch returns the inserted count. - QUALITY (HIGH): the compression-lock + compression-closed admission guards are extracted into _check_transcript_write_guards, shared by append_message and append_messages_batch (previously duplicated 23 lines that had already needed targeted fixes, NousResearch#74478). The role-gated reasoning filtering is no longer duplicated in run_agent.py — it lives at its one site inside _insert_message_rows. - EFFICIENCY (MEDIUM, measured): unbounded seed copies hold one BEGIN IMMEDIATE for seconds (10k rows ~= 2.4s; FTS triggers dominate) and monopolize the in-process write lock. append_messages_batch grows a chunk_rows param; all seed/copy call sites use chunk_rows=500. Same recovery semantics as the old per-row loops, bounded lock holds. - REUSE (MEDIUM): the two remaining per-row branch-copy loops found by the pass (gateway/slash_commands.py /branch, hermes_cli cli_commands_mixin.py branch) are converted to chunked batches too (AsyncSessionDB's generic to_thread forwarder covers the async site). Turn-flush benchmark unchanged after the refactor: 2.43 -> 0.87 ms median per 5-message flush (64% faster).
…pend_messages_batch CI-caught: test_verification_stop_caching and test_tui_gateway_server::test_native_vision_turn_persists_a_renderable_image_ref both assert on append_message.call_args, but the flush loop now calls append_messages_batch. Same class of test-fake fallout fixed in 5 other files — these two were missed.
…#38491) Re-derivation of NousResearch#38491 by @stremtec onto current main (the original is 10,119 commits behind; the hook moved into ui-tui/src/app/). The hook returned a fresh object literal every render, defeating memoization in useMainApp's consumers; useMemo over the (all-useCallback-stable) handles makes the return referentially stable. Dep array covers ALL nine returned handles incl. trimTail (the re-derivation initially omitted it - stale-closure class).
…ection get_messages_as_conversation, get_resume_conversations, and get_ancestor_display_prefix still took self._lock — the same global choke point the read-path split (WAL per-thread read-only connections) was meant to remove from every recall/browse read. These three are the hottest reads in the file: every session resume across the gateway, CLI, and ACP adapter goes through one of them, so a resume racing a burst of concurrent-session writer flushes still convoys behind them exactly like the fixed paths used to. _session_lineage_root_to_tip (the lineage walk shared by all three, plus get_conversation_root) had its own independent self._lock use and needed the same conversion — without it the outer functions still blocked on the very first line. Verified empirically: a reader thread calling all three functions while another thread holds self._lock blocked for the writer's full hold duration before the fix, and returned immediately after (SQLite 3.50.4 in this dev venv falls back to journal_mode=DELETE per the WAL-reset-bug guard, so the requires_wal-marked regression test is exercised via a local WAL-forced script instead; it still runs and passes on any runtime where WAL is actually active).
…-email chore: add contributor email mapping for ArcherQAQ
…probe sites fetch_endpoint_model_metadata's generic (non-LM-Studio) /models fetch and its llama.cpp /v1/props context-length follow-up built request URLs straight from the unrewritten candidate, unlike every other local-probe site. Both retained the multi-second dual-stack IPv6 connect penalty that _localhost_to_ipv4() exists to skip (measured on macOS: localhost 32.9ms vs 127.0.0.1 0.1ms on a dead port; ~2s on Windows). normalized stays the cache key so caching behavior is unchanged; only the outbound request target is rewritten. Re-derived from PR NousResearch#61528 onto current main (original no longer applied cleanly).
CI slice 3/7 failures: run_conversation tests pass MagicMock base_urls through the metadata probe path; re.sub raised TypeError where the old code let non-strings flow through. Preserve that contract.
…search#60800) Three fixes for the Desktop/TUI cold-start stall where the event loop is blocked for ~14s between HERMES_BACKEND_READY and the first prompt (NousResearch#60800): 1. copilot_auth: skip subprocess fallback when any Copilot env var is explicitly set (even if invalid). The user expressed token intent via env var; silently substituting a CLI token is surprising and the subprocess adds up to 5s on Windows. 2. tui_gateway/ws: run resolve_skin() via asyncio.to_thread so config loading + skin engine init do not block the WS read loop during the cold-start RPC burst. 3. web_server: extend _warm_gateway_module to pre-import the heavy module chains (auth, copilot_auth, runtime_provider, skin_engine, inventory, model_switch) that the first WS connection + RPC burst would otherwise import on the loop thread. These trigger .pyc compilation and Defender scans on Windows (15-30s per the existing comment) and were not covered by the original gateway-only warm. Tests: 5 new tests in test_cold_start_gil_stall.py + 2 new tests in test_copilot_auth.py. All 36 copilot_auth tests + 16 ws/web_server tests pass.
Review folds on the NousResearch#60807 salvage: - resolve_skin tests are behavioral (thread-ident probe + ready-frame wiring check) instead of pure source inspection, per the NousResearch#72720 pattern; a source assertion remains as belt-and-braces. - The warm-list test does REAL imports and checks sys.modules — _warm_gateway_module swallows ImportError by design, so the PR's tracking-stub test would pass even with a typo'd module name. - resolve_copilot_token logs a debug line when the env-var short-circuit skips the gh-CLI fallback (behavioral change made observable).
…ency Salvage of NousResearch#26860 (hunk 2, ported \u2014 the PR's base predates the current gateway layout by ~11.9K commits). Messaging platforms can set gateway.platforms.<key>.skip_context_files: true to skip the filesystem-heavy context-file discovery (SOUL.md, AGENTS.md, .cursorrules walks) during AIAgent construction \u2014 10-100x slower stat()/walk costs on Windows made this a real per-turn tax. Soul identity is still loaded (single small file), so the persona survives. The flag participates in _agent_config_signature so toggling it rebuilds the cached agent instead of silently reusing a prompt built under the other setting (prompt-cache correctness). The PR's hunk 1 (mtime-caching the per-turn dotenv reload) was dropped: df51ad7 mtime-cached load_config/read_raw_config and c2eda92 removed the per-turn deepcopies, capturing most of that win; the function has since gained a multiplex early-return and managed-scope overlay that the original whole-function skip would have bypassed.
… parent channel (NousResearch#77830) When a Discord channel message initiates a relay auto-thread, the thread does not exist at ingest (source.thread_id is None) — the connector creates it on its FIRST send and auto-threads any outbound carrying the reply anchor. The final reply carries that anchor, so it lands in the thread. But the tool-progress / status bubbles (the "Searching the web for..." updates and the streaming preamble) were sent with _progress_metadata=None and _progress_reply_to=None: _resolve_progress_thread_id returns None for Discord (only slack/mattermost get a synthetic thread), so the progress send had no anchor and the connector posted it FLAT in the parent channel. Result: the search-status updates leaked outside the thread while the answer threaded (staging repro 2026-08-02). The connector now stamps prospective_thread_id on the inbound (the anchor message id == the id of the thread it will create). Reuse it: when a relay-delivered Discord channel-initiate carries prospective_thread_id and has no real thread yet, carry the reply anchor (event_message_id) on both the progress metadata (reply_to_message_id) and the progress reply_to, so the connector routes the progress bubble into the SAME auto-thread as the final reply. Applied to both the tool-progress path (_progress_metadata / _progress_reply_to) and the status/interim callback path (_status_thread_metadata). Events already arriving in a real thread, DMs, and non-relay sources are untouched (guarded on delivered_via_upstream_relay + prospective_thread_id + not thread_id). Tests: two new cases in test_run_progress_topics.py — a relay Discord channel-initiate asserts every progress send carries the anchor (reply_to + metadata.reply_to_message_id + non_conversational), and an event already in a real thread asserts the synthetic-anchor path does NOT engage. Full gateway progress + relay + session suites green (228 passed).
Salvage of NousResearch#71282 (Fixes NousResearch#71281): a routable-but-dead endpoint (corp LAN address while off-VPN) blackholes TCP SYNs, so every probe in the model-metadata waterfall waits out its full connect timeout — 20+ seconds of stall per startup across detect_local_server_type, fetch_endpoint_model_metadata, and the per-model probes. A module-level blackhole cache keyed on host:port is populated when any probe observes a ConnectTimeout (httpx or requests; read timeouts deliberately excluded — an accepted connection is not a blackhole) and consulted at the top of each guarded function. 30s TTL: long enough to collapse one startup burst, short enough that VPN recovery is picked up without a restart. Guard ordering: blackhole check -> disk L2 -> HTTP waterfall, and a blackholed leg aborts the remaining legs instead of letting each stall in turn. Squash of the PR's two real commits (the branch's merge commits made it un-rebase-merge-able; content verified identical via merge-tree).
The Herald Release — voice (streaming TTS, barge-in, wake words), A2A v1.0, outbound webhooks, grounded citations, desktop platform wave. ~3,650 commits, ~1,400 PRs, ~1,200 issues closed, 650+ contributors since v0.19.0. Also: contributor audit additions (18 email mappings, bot-filter widening).
Catch-up merge from upstream v0.19-era base (5c4dc46, 2026-07-20) to the v0.20.0 release tag: 3,991 upstream commits absorbed, 106 conflicted files resolved with per-file fork provenance, fork features re-expressed on upstream's restructured surfaces: - tui_gateway: upstream extracted RPC handlers into methods_*.py; fork handler deltas (toolsets/goal/allowlist session.create, terminal turn outcomes, profile-scoped DBs, turn_outcomes history) ported into the new modules. Desktop/backend contract lands at 5. - hermes_state: fork schema columns (sender_device, turn_outcomes, session telemetry) restored into upstream's new hermes_state_common SCHEMA_SQL; schema version follows upstream (25). - Desktop: fork sidebar multi-select/bulk/drag, live context meter, spawn/goal/toolsets, dead-letter queue, and #318/#322 composer-pick isolation merged onto upstream's restructures; pagination adopts upstream's profiles_truncated model (fork totals pipeline retired). - Local-model timeout program (DFlash stall bounds, first-chunk watchdog, ask-backend-before-kill) re-expressed on upstream's Relay-refactored stream path. - Tests: union of both suites across upstream's wave-1 prune; fork node-id runner implementation kept over upstream's -k prelude. - Lockfiles regenerated from upstream's (uv.lock delta: +pytest-timeout). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
The fork runs the operator's production Hermes (desktop app, gateway, MeshBoard workers) and was pinned to v0.19.0 on an upstream base from 2026-07-20. Upstream released v0.20.0 (tag v2026.8.3) with 3,991 commits the fork lacks — including the tui_gateway handler extraction, the new sqlite WAL-vulnerability guard, session.compress RPC, and the profiles_truncated pagination model. Staying behind blocks upstream fixes and widens every future merge.
What changed
One merge commit bringing fork main onto upstream v0.20.0, resolving 106 conflicted files and re-expressing fork features on upstream's restructured surfaces:
uv lock --checkpasses.How to review
Evidence
const messagesin the desktop e2e mock server. A repo-wide repeated-dict-key sweep (ruff F601/F602) shows no merge-introduced instances (all 95 findings pre-exist on both parents).Verification
hermes cron --helpparser builds.Risks / gaps
Collaborators