Sync current Nous Hermes main into Ace patches - #36
Merged
Conversation
list_sessions_rich()'s compression-root projection called _get_session_rich_row() once per root — a separate single-row query per compression root on every session-list render. Resolve every tip id first, then fetch all tip rows in one WHERE id IN (...) query via the new _get_session_rich_rows_batch(). _get_session_rich_row() is now a thin wrapper over the batch method, so the enriched SELECT (preview + last_active) lives in exactly one place — future column changes (e.g. NousResearch#42196's include_system_prompt) only touch one query. get_compression_tip()'s chain walk is untouched; it's a genuine per-session graph walk with branch/delegate-exclusion and race handling, and batching it safely is out of scope here. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds two regression tests for the NousResearch#59077 batch: (1) _get_session_rich_rows_batch(compact_rows=True) uses the schema-derived compact projection (no system_prompt, git_branch/git_repo_root kept); (2) list_sessions_rich(compact_rows=True) threads compact_rows through the compression-tip projection call site. Mutation-checked: hardcoding compact_rows=False at the call site fails test 2.
Simplify-pass fold: SQLITE_MAX_VARIABLE_NUMBER is 999 on pre-3.32\nbuilds (which the repo still supports — the trigram-availability\nmachinery exists for exactly that class), and limit=10000\nlist_sessions_rich callers exist in web_server. Chunk inside the\nbatch helper — the single choke point — so no call site can overflow.
…ach message _dispatch_inbound_event() writes session_key → msg_id/raw_text into _processing_msg_ids and _processing_msg_texts so RecallGuardMiddleware can find and interrupt the currently-processing message. These entries were never removed after a message finished processing, causing both dicts to grow unboundedly — one persistent entry per unique session key for the lifetime of the bot. Fix: clear both entries in the _process_message_background() finally block, after super() returns. The guard compares the stored msg_id against event.message_id before popping: a concurrent pending message may have already overwritten the entry in _dispatch_inbound_event while we were running, in which case the drain task owns it and we must not clear it. When msg_id is absent (nothing was written at dispatch time) the pop is a safe no-op. Note: _msg_content_cache already bounds itself to 200 entries at the same write site; _processing_msg_ids and _processing_msg_texts had no such bound.
_build_msg_body_with_mentions() checks the TTL of each _member_cache entry and returns an empty member list when the entry is stale, but never removes the entry from the dict. Over time every group_code the bot has ever queried accumulates a permanent entry, retaining the full member list (potentially thousands of records per group) until disconnect(). Fix: delete the stale entry at the point it is detected as expired. The next call to get_group_member_list_raw() for the same group will repopulate the cache with fresh data as before. Symmetric with the existing TTL pattern in MessageDeduplicator, which evicts on access.
…regression tests Follow-up on the salvaged pair: the original guard's `not msg_id` arm let an id-less internal/synthetic event erase a tracking entry a concurrently-queued id-bearing message's drain task still needs for recall matching (id-less events never write entries in _dispatch_inbound_event, so they must never pop). Tests cover: normal cleanup, id-less non-erasure, overwritten-entry ownership handoff, TTL eviction + fresh-entry survival.
…-frizikk chore: add frizikk to AUTHOR_MAP
Z.AI has separate billing for general vs coding plans and global vs
China endpoints. On startup, detect_zai_endpoint() probes up to 4
endpoints sequentially with 8s timeout each, taking 8-9 seconds when
the first endpoints return non-200 (rate limited) before a working one
is found.
Replace the sequential loop with concurrent.futures.ThreadPoolExecutor
to probe all 4 endpoints in parallel. Results are returned in
ZAI_ENDPOINTS priority order so the preference chain is preserved.
Benchmark on macOS M4 Max, Python 3.11, Hermes v0.8.0:
Before: 8.8s (sequential: global=0.9s/429, cn=1.6s/429,
coding-global=4.3s/200, coding-cn=2.0s/200)
After: ~4.5s (single round-trip, bounded by slowest endpoint)
Signed-off-by: Merlin <merlin@merlin.me>
Rebase fold: the original PR predates ZAI_ENDPOINTS growing per-endpoint probe_models lists; the parallel worker now preserves that candidate-model fallback loop (was: scalar model). Tests (both mutation-checked): - candidate-model fallback within one endpoint worker - ZAI_ENDPOINTS priority order wins over completion order - all-fail returns None
…fy finding) The as_completed drain + `with` join made the parallel version WORSE than sequential main in the common case (first endpoint succeeds fast, others slow/unreachable): main returned at first success, the parallel version waited for every straggler. Now: after each completion, walk endpoints in priority order and return as soon as a success is unbeatable (all higher-priority probes already finished); pool uses shutdown(wait=False) so losers drain in the background. Mutation-checked: removing the early exit makes the new timing test fail (8.2s vs <1.5s).
…ext + real-transport tests Follow-ups on the salvaged bounded-read fix: - refresh flow: the non-200 branch reads a STREAMED body, which fails (ReadError/StreamClosed) once the httpx.Client context has exited — moved inside the context. Repro + regression test use a real socket server (MockTransport buffers in memory and cannot catch this). - truncation guard: >limit bodies end with ...[truncated] (mutation-checked against the is_stream_consumed fallback). - test mocks now model the streamed-read surface (is_stream_consumed, iter_bytes, client.send) so non-200 paths exercise the real bounded read.
…-aydnoktay chore: contributor email mapping for aydnOktay
…77641) unixwzrd.register@mac.com -> unixwzrd (NousResearch#74679); dai.suzuki.829@gmail.com -> hariNEzuMI928 (NousResearch#75395); lexharddrive69@gmail.com -> hdd69 (NousResearch#38470); coder@trevhome.local -> trevornk (NousResearch#76282). Needed for the B2 desktop-renderer salvage attributions.
Salvage of PR NousResearch#25350 (commits 88ffede + 2b848a0 + 3136dc6, squashed and ported): the run_agent.py prefetch site the PR gated has since moved into agent/turn_context.py's build_turn_context(), so the trivial-query gate lands there instead. - Gate the per-turn memory_manager.prefetch_all() on a trivial-prompt check so greetings/acknowledgements ('hi!', 'thanks', 'ok') no longer block the turn on provider network round-trips or inject stale context. - Extend honcho's _TRIVIAL_PROMPT_RE with greetings and a trailing punctuation class so 'hey!' / 'hello.' classify as trivial. - Add honcho classifier tests for greeting forms.
Rebase fold on the salvaged gate:
- is_trivial_prompt/TRIVIAL_PROMPT_RE move to agent/memory_provider (the
ABC both the core gate and providers already import) — one source of
truth; honcho's _TRIVIAL_PROMPT_RE now aliases it, turn_context and the
queue_prefetch_all warm path (a sibling site main grew after the PR's
base) both use it
- tests: gate tests at the prefetch call site (mutation-checked), shared
classifier tests incl. prefix-collision guards (k8s/yolo/note/supper),
and honcho dialectic-machinery tests re-driven with a substantive prompt
("hello" became trivial by design — those tests exercise thread cadence,
not the classifier)
…assifier Simplify-pass finding: sharing only the REGEX left the wrapper logic (empty/strip/slash checks) duplicated, half-defeating the no-drift goal. The classmethod now calls agent/memory_provider.is_trivial_prompt directly; _TRIVIAL_PROMPT_RE stays as a class attr for backward compatibility with any external referents.
…it frame scheduleDeltaFlush's adaptive floor is driven by lastFlushCostRef, but runFlush only timed flushQueuedDeltas(), the synchronous store write. While a session streams, syncSessionStateToView defers the $messages publish (React commit + Streamdown re-parse) to its own rAF, so the measured cost stayed near zero and the floor collapsed to the fixed 33ms path no matter how expensive the real commit was. runFlush now records the write cost as a fallback, then extends the measurement through a rAF registered after the view-sync one: it runs in the same frame right after the deferred commit, and the rAF timestamp marks frame start so only in-frame work is counted, not the vsync wait. A stale callback from before a newer flush is ignored, and a hidden renderer that never fires rAF keeps the write-cost fallback.
All three desktop reconnect loops (primary gateway boot, secondary multi-profile gateway pool, plugin event socket) used bare exponential backoff with no jitter. After a gateway restart every disconnected client redials on the exact same schedule, so the reconnect attempts land in lockstep instead of spreading out -- a burst that can starve the gateway's file descriptors while it's still coming back up. Add reconnect-backoff.ts implementing AWS-style full-jitter backoff (random delay in [0, min(cap, base * 2^attempt))) and wire it into all three call sites in place of their local Math.min/2**attempt math. Manual reconnect paths already reset the attempt counter and bypass the timer entirely -- unchanged.
… count With the full-jitter backoff (300ms base) six attempts can elapse in ~9s, so the old RECONNECT_ESCALATE_AFTER=6 attempt threshold raised the recoverable boot error during a brief post-boot blip — breaking the 'a remote that drops post-boot keeps looping with NO boot.error' contract. Escalate after RECONNECT_ESCALATE_AFTER_MS (45s, matching the old deterministic 1->15s ladder's calibration) elapsed since the first failed reconnect of the episode. Reset on clean open, manual/wake reconnect, and soft switch, preserving the reset-on-success path.
…ousResearch#77665) Needed for the NousResearch#62082 curator toolset-pin salvage attribution.
…ended it early
The comment above .btn-arc contained 'bg-*/', whose */ terminated the comment mid-sentence, leaving 'text-* variant utilities. */ .btn-arc {' as an invalid prelude — CSS error recovery can drop the whole .btn-arc rule. Reword so no */ appears inside the comment.
Extracted from NousResearch#59352 by @rerdi92 (the rest of that PR — a month-stale icons.ts rewrite and a chunk-size warning-ceiling bump — is superseded/masking and was not salvaged).
…ound)" (NousResearch#78687) hermes debug share runs on the backend. A desktop app connected to a remote, docker, or SSH backend writes desktop.log on the client machine, so the bundle can never contain it — and the report rendered that as a bare "(file not found)", which reads as "the app logged nothing" and sends triage after a client-side bug it cannot see. Name the writer and the path to collect by hand. Backend-written logs are unchanged, a present desktop.log is still captured, and an empty one still reports "(file empty)" — the app ran and logged nothing is a different fact from the file being on another host.
…script-window fix(desktop): oversized sessions open without crashing the renderer
… throttle A pool with only one usable (non-DEAD) credential has nothing to rotate to. On a transient throttle (429 rate-limit, 403 edge-throttle, 5xx) the offending key was benched for a full hour (EXHAUSTED_TTL_429/DEFAULT), so single-key / no-fallback setups got an hour of hard failures for a throttle that resets in seconds. The pool already special-cases 401 to recover quickly for single-key setups; extend that to transient throttles when the credential is the sole non-DEAD entry. 402 (billing/quota) keeps the full bench — a quick retry can't help. Provider-supplied reset_at still overrides. Adds tests covering sole 429/403 recovery, 402 full-bench, and multi-key (no early recovery).
next_available_at() was computing the full 1-hour TTL for a sole credential on a 429, contradicting the 60s cooldown in _available_entries. The fallback restore gate (agent_runtime_helpers) uses next_available_at to decide when to switch back from fallback to primary — so the agent stayed on fallback for an hour instead of ~60s. Add sole_credential computation in next_available_at mirroring _available_entries, and a test verifying the short cooldown propagates.
The sole-credential cooldown sized the bench from the raw HTTP status, but 403 is overloaded: error_classifier maps OpenRouter's "key limit exceeded" and xAI's spending-limit block to FailoverReason.billing, while an edge throttle with the same status is transient. Only 402 was excluded from the short cooldown, so a spent account on a single key retried every 60 seconds and re-failed forever. Thread the classified reason from recover_with_credential_pool through mark_exhausted_and_rotate to _exhausted_ttl. Billing keeps the full bench regardless of status; everything else transient still recovers in 60s. The verdict is stored on the entry (_EXTRA_KEYS, so it persists to auth.json) — without that a restart would re-read a bare 403 and downgrade the bench. Tests: sole billing-403 stays benched, survives reload, unclassified 403 still recovers; call-site coverage that the reason actually reaches the pool. Three existing kwargs assertions updated for the new argument.
Three fakes pin mark_exhausted_and_rotate's signature explicitly and broke on the new argument. They now assert it rather than just tolerate it — the xAI spending-limit case is exactly the billing-403 this fixes, so it should be pinning `failure_reason == "billing"`.
export_profile() accepts extra_files (root-relative filename -> text) so a
caller can stage companion files into the archive; the desktop uses it for
desktop.json, its appearance/interface overlay, now part of the default
profile's export allow-list.
New routes wrapping the existing hermes profile export/import machinery:
- POST /api/profiles/{name}/export (extra_files + optional output path)
- POST /api/profiles/import (returns the bundled desktop overlay)
- GET /api/profiles/{name}/desktop-overlay
Paths cross the API, not bytes - the desktop's native dialogs and its
local/pooled backends share a filesystem.
/export [profile] [-o output.tar.gz] bundles a profile into the shareable archive; /import <archive> [--name <name>] adopts one as a new profile (wrapper alias created when safe). Registry-driven, cli_only, so the CLI and TUI both pick them up in autocomplete and help.
…skills Export stages desktop.json (skin + mode, bundled user-theme definitions, rail color, layout tree) into the CLI's own profile archive; import applies it, so the receiver gets the whole look as a ready-to-use profile. Doors: Export/Import profile... in Cmd-K, an import button beside the rail's +, and Export in each profile square's context menu. New selectSavePath IPC (native save dialog); credentials never leave the machine (CLI filter).
…search#78815) * fix(discord): reject empty outbound messages * test(discord): cover empty final reply backfill state Missed-message backfill decides what to replay from discord_messages, so a dropped final reply must be recorded as failed by the new guard the same way the exception path records one — otherwise the reply is both never sent and never retried. Co-authored-by: Jony <619963502@qq.com> * chore: map 619963502@qq.com to zyz619963502zyz for PR NousResearch#73449 salvage --------- Co-authored-by: Jony <619963502@qq.com>
Adds a last_read_at watermark to the sessions table so surfaces (CLI, TUI, desktop) can badge unread conversations. Read state derives from the watermark vs latest activity, so new messages flip a conversation back to unread with zero writes on the message path. NULL means never tracked, so shipping the column doesn't badge pre-existing history. set_session_read() stamps the whole compression lineage, matching the archive/pin semantics; list_sessions_rich() rows carry a derived `unread` key. DB layer only — no surface exposes it yet.
…lay-tool-metrics feat(observability): aggregate bounded tool metrics
movePanes/reorderPanesInGroup/mergeZonesWithPane now take a block of pane ids in strip order: the lead pane decides the drop geometry (slot, split, span-merge) and the rest stack in behind it, with the pressed tab fronting at the destination. The tab-selection store holds the block (Chrome grammar: toggle, anchor range, collapse on plain click), drag-session carries it — every dragged tab dims, the insertion slot skips the whole block, and a landed drop spends the selection while a deny-area release keeps it for a retry.
Chrome's grammar on every zone tab strip: Shift-click ranges from the anchor, ⌥-click (Ctrl-click off-Mac — ⌘ stays close, ⌃ stays the macOS context menu) toggles, plain click collapses back to one tab. Selected tabs wear an accent wash; dragging any of them carries the block — the ghost chip counts it — into a strip slot, a zone edge, or a Shift-span, so three tabs land in a new zone as one gesture.
…ad-state Sessions track read/unread
…rkspace A session created in the wrong directory needs its cwd corrected after the fact. session.cwd.set only reaches live runtime sessions, so cold rows were stuck. The new RPC targets the persisted row by session_key, validates the folder, and REPLACES the git branch/root identity (update_session_cwd grows a replace_git_meta flag) so the project tree's grouping follows the move instead of pinning the session under the project it left via a stale git_repo_root. A live idle agent bound to the row is re-anchored through the runtime path; a mid-turn session refuses with 'session busy'. Runs on the RPC pool — the git probes are subprocesses.
'Move to project' submenu in the session actions menu (kebab and right-click, via the shared MenuKit) listing every project with a folder except the current owner. Picking one calls session.workspace.move at the project root, mirrors the new cwd/branch/root into the $sessions cache, and refreshes the tree so the row hops immediately.
shutil.make_archive writes PAX with fractional-mtime records, which macOS
Archive Utility rejects ("Error 94 - Bad message") on double-click. Write
the profile archive with tarfile in GNU format instead: integer mtimes,
longlink for deep paths, extracts under Finder, bsdtar, and gnutar alike.
Verified against /usr/bin/tar (bsdtar) with >100-char member paths.
…drag Select multiple tabs and drag them to a zone together
…ve-project Right-click a session to move it into another project
Share your whole setup: export/import profiles with theme, layout, and skills
…404ing (NousResearch#78856) Selecting an NVIDIA NIM model whose id reached config without the nvidia/ prefix produced a bare "HTTP 404: 404 page not found" — retried three times, never naming the model. It reads exactly like an outage or an auth failure, which is where the Discord thread spent its time before the id was spotted. normalize_model_for_provider() had no branch for nvidia, so a bare id passed straight through to the API. Repair it from the provider's curated catalogue: a bare name that matches exactly one entry modulo the prefix gets it back. That's a lookup, not a guess — build.nvidia.com also fronts local NIM containers and third-party models, and anything absent from the catalogue is left alone. Because the repair runs on every runtime setup, an already-broken config self-heals on the next turn and prints what it changed. If a bare id still reaches the wire, the 404 now explains itself. The classifier consults the same catalogue: a prefix-less id the provider only serves as vendor/model is a deterministic failure, so it classifies as model_not_found instead of burning three retries on a retryable "unknown", and the error trace names the id to use. Fixes NousResearch#78796
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.
Automated fail-closed upstream sync. Locally verified head: 313fb1f