Skip to content

Merge upstream v2026.8.3 — Hermes v0.20.0 catch-up - #352

Merged
OmarB97 merged 3992 commits into
mainfrom
sync/upstream-v0.20.0
Aug 4, 2026
Merged

Merge upstream v2026.8.3 — Hermes v0.20.0 catch-up#352
OmarB97 merged 3992 commits into
mainfrom
sync/upstream-v0.20.0

Conversation

@OmarB97

@OmarB97 OmarB97 commented Aug 4, 2026

Copy link
Copy Markdown
Owner

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:

  • tui_gateway: upstream extracted ~7,000 lines of RPC handlers into methods_*.py modules (installed over server.py with rebound globals). Fork handler deltas ported into the new modules: session.create toolsets/goal/allowed-commands pins, claim-then-dispatch terminal turn outcomes, profile-scoped session DBs, turn_outcomes in history payloads, image.attach path hinting. Desktop/backend contract lands at 5 (upstream's), verified no fork-only RPC is stranded.
  • hermes_state: fork schema (sender_device, turn_outcomes table, session telemetry columns, indexes) restored into upstream's new hermes_state_common SCHEMA_SQL; schema version follows upstream (25); fork's cost-repair hook re-attached in hermes_state_schema.
  • Desktop renderer: fork sidebar multi-select/bulk/drag program, live context meter, spawn/goal/toolsets, queue dead-letter ledger, and composer-pick isolation (fix(desktop): stop a spawned session's model from moving the composer pick #318/fix(desktop): stop a session's effort and fast mode moving the composer pick #322) merged onto upstream's restructures. Pagination adopts upstream's profiles_truncated model (the fork per-profile COUNT totals pipeline is retired — nothing seeds it upstream anymore). /compress keeps upstream's dedicated session.compress RPC wrapped in the fork's interrupt-first busy choreography.
  • Local-model timeout program (DFlash stall bounds, first-chunk watchdog, ask-backend-before-kill): re-expressed on upstream's Relay-refactored stream path (first-chunk latch ported into _open_stream/_accept_stream_chunk).
  • hermes_cli: fork fallback-policy machinery, inventory normalization, transactional profile distributions (with upstream's owned-paths contract for omitted allowlists), cron per-job pins unified on the fork's _resolve_model_override resolver.
  • Tests: union of both suites across upstream's wave-1 prune (46.8k → 28.1k tests); fork node-id runner implementation kept over upstream's -k prelude; fork tests modernized where upstream deliberately evolved contracts (WAL forced-delete guard, compression-closed guard, failed_model forwarding).
  • Lockfiles: uv.lock and package-lock.json reset to upstream's and regenerated — uv.lock delta vs upstream is exactly +pytest-timeout 2.4.0; uv lock --check passes.
  • Ruff policy: tui_gateway/methods_*.py exempted from the fork F821 ratchet with rationale (handler globals are rebound at install time; the rule cannot model it).

How to review

  1. Read the merge commit message, then this body's What changed.
  2. tui_gateway/methods_session.py + methods_prompt.py — the ported fork handler deltas (search for toolsets/goal/allowed_commands in session.create, _dispatch_claimed_prompt in prompt.submit).
  3. hermes_state_common.py SCHEMA_SQL — fork columns/tables restored next to upstream's schema.
  4. apps/desktop/src/store/session.ts and app/chat/sidebar/index.tsx — the pagination model switch and preserved bulk/multi-select surface.
  5. apps/desktop/src/app/session/hooks/use-prompt-actions/slash.ts — the compress choreography graft.
  6. agent/chat_completion_helpers.py — the first-chunk watchdog on the Relay path.
  7. Tests: treat the suite as the oracle; every fork feature above has its pinning tests preserved or modernized.

Evidence

  • Conflict resolution ran with per-file fork provenance (git log origin/main --not upstream/main per file) — the v0.18.2-merge failure mode (silent fork-symbol drops) was specifically hunted: three such drops were found in auto-merged (non-conflicted) files and repaired: the $sessionProfileTotals atom, tools/cronjob_tools._resolve_model_override, and the fork schema in hermes_state_common.py (fresh-DB repro: 192 test failures before, 0 after).
  • Auto-merge stacking artifacts found and fixed: duplicate --model/--provider argparse registrations + duplicate kwargs in hermes_cli/cron.py (parse-time SyntaxError), duplicate const messages in 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).
  • Desktop/backend contract verified coherent at 5/5; 130 RPC methods post-merge vs 122 fork, none lost.

Verification

  • ruff (fork gate: PLW1514 + F821, preview): clean, zero findings.
  • Python: 35 oracle suites run per-file with isolated HERMES_HOME and the CI extras installed — 34 fully green after fixes, including test_hermes_state 190, test_tui_gateway_server 527, local_stream_timeout 121, auxiliary_client package 360, web_server 137, cron 11 + scheduler 64, run_agent 236, run_tests_parallel 15, declared-allowlist 74. The full 8-shard suite + e2e runs in this PR's CI.
  • Desktop: tsc --noEmit clean; renderer vitest 3,618 passed / 0 failed (409 files); eslint clean on touched files.
  • uv lock --check passes; npm ci from upstream lockfile clean.
  • Import smoke: all core modules including tui_gateway.server (exercises the methods install rebind) import cleanly; hermes cron --help parser builds.

Risks / gaps

  • Upstream's 3,991 commits carry behavior changes beyond the conflict surface; the fork test oracle covers fork features, upstream CI shards cover the rest — accepted risk for a catch-up merge of this size.
  • config.set key=model during a running turn now defers (upstream pending_model_switch) instead of erroring 4009 — deliberate upstream behavior adopted; low risk.
  • Fork per-profile COUNT totals retired for upstream's profiles_truncated; count labels that read them were dropped — no work item needed, superseded feature.
  • agent/session_telemetry.py still calls update_token_counts (both writers exist post-merge); migrating to upstream's queue_token_counts is a perf follow-up — low risk.
  • The legacy-gateway /compress fallback keeps pre-interrupt but not the settle-edge retry — accepted tradeoff; restoring it would re-break upstream's fallback dispatch contract.
  • test_write_json_serializes_concurrent_writes flakes in full-file runs from a leaked writer thread in an earlier test — pre-existing before this merge, passes in isolation, already tracked as its own task.

Collaborators

  • @OmarB97 (operator)
  • Claude Fable 5 (merge orchestration + verification)

OutThisLife and others added 30 commits August 2, 2026 22:08
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).
…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.
ehz0ah and others added 29 commits August 3, 2026 20:35
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>
@OmarB97
OmarB97 merged commit e36a802 into main Aug 4, 2026
23 of 24 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.