hermes-bsd/scripts/release.py

2122 lines
104 KiB
Python
Raw Normal View History

#!/usr/bin/env python3
"""Hermes Agent Release Script
Generates changelogs and creates GitHub releases with CalVer tags.
Usage:
# Preview changelog (dry run)
python scripts/release.py
# Preview with semver bump
python scripts/release.py --bump minor
# Create the release
python scripts/release.py --bump minor --publish
# First release (no previous tag)
python scripts/release.py --bump minor --publish --first-release
# Override CalVer date (e.g. for a belated release)
python scripts/release.py --bump minor --publish --date 2026.3.15
"""
import argparse
import json
import re
import shutil
import subprocess
import sys
from collections import defaultdict
from datetime import datetime
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parent.parent
VERSION_FILE = REPO_ROOT / "hermes_cli" / "__init__.py"
PYPROJECT_FILE = REPO_ROOT / "pyproject.toml"
# ACP Registry manifest must stay version-locked with pyproject.toml.
# tests/acp/test_registry_manifest.py enforces this lockstep so the release
# bump touches both files atomically.
ACP_REGISTRY_MANIFEST = REPO_ROOT / "acp_registry" / "agent.json"
# ──────────────────────────────────────────────────────────────────────
# Git email → GitHub username mapping
# ──────────────────────────────────────────────────────────────────────
# Auto-extracted from noreply emails + manual overrides
AUTHOR_MAP = {
"peterhao@Peters-MacBook-Air.local": "pinguarmy",
"barronlroth@gmail.com": "barronlroth",
"ondrej.drapalik@gmail.com": "OndrejDrapalik",
"tomasz.panek@gmail.com": "tomekpanek",
"philipadsouza@gmail.com": "PhilipAD",
"zhuhaoyu0909@icloud.com": "underthestars-zhy",
"raysun12142006@gmail.com": "yanxue06",
"alberto.regalado@ymail.com": "ARegalado1",
"alchemistchaos@protonmail.com": "AlchemistChaos", # co-author only
"gilad@smiti.ai": "giladbau",
"yusufalweshdemir@gmail.com": "Dusk1e",
"804436395@qq.com": "LaPhilosophie",
feat(desktop+gateway): remote media relay — attach images/PDFs and display gateway images over the network Desktop connected to a remote gateway can now attach images and PDFs and display agent-written images. Previously the desktop passed a LOCAL file path to image.attach; on a remote gateway that path doesn't exist, so the image was silently dropped ("skipped unreadable path") and the vision model never saw it. The reverse direction was also broken — images the agent wrote on the gateway rendered as dead links in the remote client. Gateway (tui_gateway/server.py): - image.attach_bytes: base64 byte upload written into the gateway's own images dir and queued via the existing native-image-attach pipeline. Magic-byte extension sniffing, data-URL prefix + whitespace tolerance, 25 MB cap, structured error codes. Accepts content_base64/filename (canonical) and data/ext (older-desktop aliases). - pdf.attach: renders each page to PNG via pdftoppm (poppler-utils) at 150 DPI and queues the pages as images; 50 MB / 25-page caps. Accepts host path or base64 upload. - Shared helpers (_decode_attach_base64, _sniff_image_ext, _queue_attached_image) so the two methods and the existing image.attach don't duplicate logic. Gateway (hermes_cli/web_server.py): - GET /api/media: returns a gateway-local image as a base64 data URL so remote clients can display it. Auth-gated like every /api route, extension allowlist + size cap, AND confined to the gateway's own media roots (images/screenshots/cache, resolved symlink-safe) so an authed caller can't read image-extension files anywhere on disk. Desktop (apps/desktop): - syncImageAttachmentsForSubmit uploads bytes via image.attach_bytes when the connection mode is 'remote'; the local fast path is unchanged. - media.ts gains isRemoteGateway() + gatewayMediaDataUrl(); directive-text and markdown-text fetch images over /api/media in remote mode. Consolidates the competing remote-media PRs (#38876, #40317, #21908, #39437) into one coherent implementation, taking the strongest parts of each and adding shared-helper cleanup plus the /api/media root-confinement hardening on top. The per-profile gateway switching from #38876 is intentionally left out as a separable feature. TUI file uploads (#40492) remain a separate surface. Tested: 11 new tui_gateway tests + 5 /api/media endpoint tests + desktop media.remote unit tests; full tui_gateway + web_server suites green (472 passed); tsc -b clean; E2E verified the full attach→disk→queue and gateway-path→data-URL display round-trip plus the out-of-root security block. Co-authored-by: Max Mitcham <maxmitcham@mac.home> Co-authored-by: Justlrnal4 <Justlrnal4@users.noreply.github.com> Co-authored-by: Chris Cook <ccook@nvms.com> Co-authored-by: Thomas Paquette <thomas.paquette@gmail.com>
2026-06-07 04:37:38 -07:00
"maxmitcham@mac.home": "maxtrigify",
"ccook@nvms.com": "ccook1963",
2026-06-08 15:16:20 +05:30
"kristian@agrointel.no": "kristianvast",
feat(desktop+gateway): remote media relay — attach images/PDFs and display gateway images over the network Desktop connected to a remote gateway can now attach images and PDFs and display agent-written images. Previously the desktop passed a LOCAL file path to image.attach; on a remote gateway that path doesn't exist, so the image was silently dropped ("skipped unreadable path") and the vision model never saw it. The reverse direction was also broken — images the agent wrote on the gateway rendered as dead links in the remote client. Gateway (tui_gateway/server.py): - image.attach_bytes: base64 byte upload written into the gateway's own images dir and queued via the existing native-image-attach pipeline. Magic-byte extension sniffing, data-URL prefix + whitespace tolerance, 25 MB cap, structured error codes. Accepts content_base64/filename (canonical) and data/ext (older-desktop aliases). - pdf.attach: renders each page to PNG via pdftoppm (poppler-utils) at 150 DPI and queues the pages as images; 50 MB / 25-page caps. Accepts host path or base64 upload. - Shared helpers (_decode_attach_base64, _sniff_image_ext, _queue_attached_image) so the two methods and the existing image.attach don't duplicate logic. Gateway (hermes_cli/web_server.py): - GET /api/media: returns a gateway-local image as a base64 data URL so remote clients can display it. Auth-gated like every /api route, extension allowlist + size cap, AND confined to the gateway's own media roots (images/screenshots/cache, resolved symlink-safe) so an authed caller can't read image-extension files anywhere on disk. Desktop (apps/desktop): - syncImageAttachmentsForSubmit uploads bytes via image.attach_bytes when the connection mode is 'remote'; the local fast path is unchanged. - media.ts gains isRemoteGateway() + gatewayMediaDataUrl(); directive-text and markdown-text fetch images over /api/media in remote mode. Consolidates the competing remote-media PRs (#38876, #40317, #21908, #39437) into one coherent implementation, taking the strongest parts of each and adding shared-helper cleanup plus the /api/media root-confinement hardening on top. The per-profile gateway switching from #38876 is intentionally left out as a separable feature. TUI file uploads (#40492) remain a separate surface. Tested: 11 new tui_gateway tests + 5 /api/media endpoint tests + desktop media.remote unit tests; full tui_gateway + web_server suites green (472 passed); tsc -b clean; E2E verified the full attach→disk→queue and gateway-path→data-URL display round-trip plus the out-of-root security block. Co-authored-by: Max Mitcham <maxmitcham@mac.home> Co-authored-by: Justlrnal4 <Justlrnal4@users.noreply.github.com> Co-authored-by: Chris Cook <ccook@nvms.com> Co-authored-by: Thomas Paquette <thomas.paquette@gmail.com>
2026-06-07 04:37:38 -07:00
"thomas.paquette@gmail.com": "RyTsYdUp",
"techxacm@gmail.com": "ProgramCaiCai",
"266365592+bmoore210@users.noreply.github.com": "bmoore210",
"157839748+psionic73@users.noreply.github.com": "psionic73",
"manishbyatroy@gmail.com": "manishbyatroy",
"chilltulpa@gmail.com": "TheGardenGallery",
"al@randomsnowflake.me": "randomsnowflake",
"zakame@zakame.net": "zakame",
"152110621+jiangkoumo@users.noreply.github.com": "jiangkoumo",
"834740219@qq.com": "ViewWay",
"matt@vestigial.dev": "m4dni5",
"harjoth.khara@gmail.com": "harjothkhara",
"129007007+HeLLGURD@users.noreply.github.com": "HeLLGURD",
"290859878+synapsesx@users.noreply.github.com": "synapsesx",
"dirtyren@users.noreply.github.com": "dirtyren",
"mharris@parallel.ai": "NormallyGaussian",
"ted.malone@outlook.com": "temalo",
"adityamalik2833@gmail.com": "alarcritty",
"islam666@users.noreply.github.com": "islam666",
"mnajafian@nvidia.com": "mnajafian-nv",
"25539605+lsaether@users.noreply.github.com": "lsaether",
"30080538+JimStenstrom@users.noreply.github.com": "JimStenstrom",
"rod.boev@gmail.com": "rodboev",
"70290504+dangelo352@users.noreply.github.com": "dangelo352",
"zhaolei.vc@bytedance.com": "zhaoleibd",
"jeffrobodie@gmail.com": "jeffrobodie-glitch",
"kyssta-exe@users.noreply.github.com": "kyssta-exe",
"ali.zakaee.1997@gmail.com": "ITheEqualizer",
fix(desktop): attachments on Enter, IME composition, scroll, fetchJson resets (salvage #38502) (#38677) * fix(desktop): critical fixes — attachments, IME composition, scroll, fetchJson DC2: Pass attachments to onSubmit() on direct Enter submit and call clearComposerAttachments(). Previously attachments were silently dropped — only text was sent while attachment pills remained visible. DH1: Add 'open' to ThinkingDisclosure ResizeObserver effect deps. When the disclosure toggles, refs point to new DOM but the observer wasn't reattached, breaking live-scroll preview after expand/collapse and leaking detached DOM nodes. DH3+DH4: Add composition tracking via composingRef (set by compositionstart/compositionend). Guards handleEditorInput (skip preedit state writes), handleEditorKeyDown (prefer composingRef over unreliable isComposing), and form onSubmit (prevent IME Enter from triggering submission). Fixes IME Enter message splitting and preedit text leaking into app state on CJK input. DH6: Add res.on('error', reject) to fetchJson response stream. Without this, a TCP reset mid-transfer left the promise hanging forever, freezing the desktop UI. All TypeScript compiles cleanly. * chore: add copii.list@gmail.com to AUTHOR_MAP (stremtec) * fix(desktop): prevent scroll snap-back during streaming, atomic config writes DH2: Defer pinToBottom() in useLayoutEffect to rAF so that browser scroll/wheel events from the current frame are processed first. Previously an immediate pinToBottom() could snap the viewport back to bottom against the user's trackpad scroll-up intent during streaming — the wheel event hadn't fired yet so stickyBottomRef was still true. DH7: Add writeFileAtomic() helper (write to .tmp then rename) and use it in writeDesktopConnectionConfig, writeDesktopUpdateConfig, and writeBootstrapMarker. Prevents partial writes on crash/power loss that would corrupt JSON config files, requiring manual repair. * fix(desktop): guard nativeTheme listener from duplicates, invalidate connection config cache DM9: Guard nativeTheme.on('updated') with a one-shot flag so that multiple createWindow() calls (e.g. macOS activate after all windows closed) don't accumulate duplicate listeners on the process-wide singleton. DM3: Add mtime-based cache invalidation to readDesktopConnectionConfig. Previously the cache was populated once and never invalidated — if an external tool modified connection.json, the desktop ignored the change until restart. Now re-reads when the file's mtime differs. * fix(desktop): widen fetchJson res.on('error') to sibling fetch + sort JSX props Follow-up to salvaged #38502: - resourceBufferFromUrl had the same mid-stream-reset hang class as fetchJson (req.on('error') present, res.on('error') missing). Add the response-stream error handler so a TCP reset during body read rejects instead of leaving the promise unsettled. - Sort the new onComposition* JSX props to satisfy perfectionist/sort-jsx-props (was an introduced eslint error in the composer). --------- Co-authored-by: asill-livestream <copii.list@gmail.com>
2026-06-03 21:38:58 -07:00
"copii.list@gmail.com": "stremtec",
"solaiagent@gmail.com": "solaitken",
"cryptoworlldz@gmail.com": "worlldz",
"prostoandrei9@gmail.com": "vladkvlchk",
"116314616+ThyFriendlyFox@users.noreply.github.com": "ThyFriendlyFox",
"liliangjya@gmail.com": "truenorth-lj",
"16943149+nepenth@users.noreply.github.com": "nepenth",
"ben.bartholomew@vectorize.io": "benfrank241",
"74339271+SaguaroDev@users.noreply.github.com": "SaguaroDev",
"subw3@mail2.sysu.edu.cn": "Subway2023",
"trevin@trevinchow.com": "tmchow",
"zhipengli@thebrainly.ai": "a1245582339",
"mathijs.vd.hurk@gmail.com": "mathijsvandenhurk",
"david.gutowsky@gmail.com": "davidgut1982",
"drpelagik@gmail.com": "SeaXen",
"lengr@users.noreply.github.com": "LengR",
"Kewe63@users.noreply.github.com": "Kewe63",
"kewe.3217@gmail.com": "Kewe63",
fix(tools): don't compound-rewrite spawn_via_env background wrappers Background tasks on non-local backends (SSH/Docker/Modal/Daytona/Singularity) go through `ProcessRegistry.spawn_via_env`, which builds a hand-crafted, shell-safe wrapper: mkdir -p T && ( nohup bash -lc CMD > LOG 2>&1; rc=$?; ... ) & echo $! > PID && cat PID `BaseEnvironment.execute()` unconditionally ran `_rewrite_compound_background` on every command, including this wrapper. The rewrite (meant to defuse the `A && B &` subshell-wait trap for user commands) turns `( ... ) & echo $!` into `{ ( ... ) & } echo $!` — note `} echo` with no separator, which is a bash syntax error. The wrapper then never produces a PID, the redirected output file is never created, and the agent sees an immediate exit code -1. This breaks *every* background launch on a non-local backend (e.g. a simple count-and-redirect script over SSH), not just edge cases. Fix: - Add `rewrite_compound_background: bool = True` to `BaseEnvironment.execute()` (and the `BaseModalExecutionEnvironment` override, which accepts and ignores it). Default preserves existing behavior; the user foreground terminal path still rewrites. - `spawn_via_env` passes `rewrite_compound_background=False` so its already shell-safe wrapper is left intact. - Treat a wrapper that produces no PID as a failed launch (mark the session exited with a real exit code instead of exposing a fake running session), and don't register/checkpoint a session that never started. Verified empirically: with the rewrite skipped, the wrapper is valid bash, launches the process, captures the PID, and writes the log/pid/exit files; the old rewritten form fails `bash -n` with a syntax error. Based on #33756 by @CharZhou (extracted from a multi-feature branch; the unrelated image_gen / docker-media changes are not included here). Co-authored-by: CharZhou <17255546+CharZhou@users.noreply.github.com>
2026-06-01 00:05:10 +05:30
"17255546+CharZhou@users.noreply.github.com": "CharZhou",
"metalclaudbot@gmail.com": "HashClawAI",
"tonybear55665566@gmail.com": "TonyPepeBear",
"kaspersniels@gmail.com": "nielskaspers",
"daxxpasquini@gmail.com": "bpasquini",
"kurobaryo@gmail.com": "kurobaryo",
"scubamount@users.noreply.github.com": "scubamount",
"251514042+youngstar-eth@users.noreply.github.com": "youngstar-eth",
"155192176+alelpoan@users.noreply.github.com": "alelpoan",
"aman@abacus.ai": "Aman113114-IITD",
"octavio.turra@gmail.com": "octavioturra",
"524706+Twanislas@users.noreply.github.com": "Twanislas",
"9592417+adam91holt@users.noreply.github.com": "adam91holt",
fix(agent): fallback immediately on provider content-policy blocks (#33883) * fix(agent): fallback immediately on provider content-policy blocks Provider safety-filter refusals (e.g. OpenAI Codex 'flagged for possible cybersecurity risk', OpenAI moderation 'violates our usage policies', Anthropic safety-system rejections, Azure content_filter) are deterministic decisions about a specific prompt. Retrying the same prompt up to api_max_retries times just reproduces the same refusal and burns paid attempts before surfacing the generic 'API failed after 3 retries — <provider message>' to Telegram / cron with no indication that the failure came from the model provider rather than Hermes itself. Classify these as a new FailoverReason.content_policy_blocked (non-retryable, should_fallback=True) and route them through the existing is_client_error path so the loop: - skips the 3x retry backoff - activates a configured fallback model immediately - emits a clear provider-safety message to the user (not the generic 'Non-retryable error (HTTP None)') and surfaces actionable guidance when no fallback is configured (rephrase, narrow context, or set fallback_model in hermes config) - returns a final_response that explicitly tells the user this came from the model provider, so gateway delivery is unambiguous and cron last_status reflects the safety block rather than a vague 'agent reported failure' Patterns are intentionally narrow — verbatim refusal phrasings keyed to specific provider safety pipelines, not generic words like 'policy' or 'violation' that would collide with billing / format / auth errors. Regression guards in test_18028_content_policy_blocked.py verify billing 402s, generic 400s, and OpenRouter account-level provider_policy_blocked remain distinct classifications. Salvaged from #18164 onto current main (file restructure: loop logic moved from run_agent.py to agent/conversation_loop.py, _emit_status → _buffer_status), broadened patterns beyond the original OpenAI Codex cybersecurity case to cover OpenAI moderation, Anthropic safety system, and Azure content_filter; added user-actionable guidance and a clear final_response so cron/gateway surfaces the policy block instead of a generic non-retryable error, and added a regression-guard test module mirroring the is_client_error predicate. Addresses #18028. Co-authored-by: Kuan-Chieh Huang <kchuang1015@users.noreply.github.com> * chore: add kchuang1015 to AUTHOR_MAP --------- Co-authored-by: Kuan-Chieh Huang <kchuang1015@users.noreply.github.com>
2026-05-28 07:28:24 -07:00
"kchuang1015@users.noreply.github.com": "kchuang1015",
refactor(supermemory): session-level ingest + kebab aliases (salvaged from #32487) (#38756) * refactor(supermemory): session-level conversation ingest + kebab tool aliases Salvaged from #32487 (by @MaheshtheDev), rebased onto current main. - sync_turn now buffers cleaned turns; the full session is ingested once at session end / switch / shutdown via the conversations endpoint - ingest_conversation() accepts and forwards functional document metadata (type, session_id, message_count, partial) - register kebab-case tool aliases (supermemory-save/search/forget/profile) alongside the snake_case names - README + docs (EN/zh-Hans) updated for the simplified session model Source/vendor-attribution removed per project policy (no telemetry): dropped x-sm-source header, sm_source metadata, and sm_capture_mode tags. Preserved the post-branch atomic_json_write(mode=0o600) hardening that the PR's stale base had reverted. Updated provider tests for the new behavior and added maheshthedev@gmail.com to release.py AUTHOR_MAP. Co-authored-by: alt-glitch <balyan.sid@gmail.com> * feat(supermemory): restore x-sm-source for Spaces routing Reinstates x-sm-source: hermes (SDK default_headers + conversations POST) and sm_source: hermes document metadata. Per @Dhravya (Supermemory), this is a functional routing key, not telemetry: it groups Hermes writes into a dedicated "Hermes" Space in the Supermemory app so users can filter and bulk-manage memories per source agent. sm_capture_mode remains dropped (appears analytics-only; Spaces are routed by sm_source) pending confirmation. Adds README note + a unit test covering _merge_metadata sm_source stamping and legacy source->type migration. --------- Co-authored-by: Mahesh Sanikommu <maheshthedev@gmail.com>
2026-06-04 11:50:02 +05:30
"maheshthedev@gmail.com": "MaheshtheDev",
fix: batch of small robustness/correctness fixes from @kyssta-exe Salvages 8 distinct fixes from a batch of PRs by @kyssta-exe, reapplied onto current main (original branches were stale) with a few refinements. - cron(jobs.py): load_jobs() validates top-level JSON shape — a bare list auto-repairs into the {"jobs": [...]} dict; scalars/null raise a clear RuntimeError instead of an uncaught AttributeError that took down the whole cron subsystem (#37065, closes #36867). - web(web_server.py): close the per-action log file handle after Popen so the parent stops leaking one fd per spawned action (#36843). - web(web_server.py): DELETE /api/env returns 400 for invalid key names instead of a misleading 500, mirroring PUT /api/env (#36840). - gateway(gateway.py): read /proc/<pid>/cmdline inside a with-block so the fd is released immediately instead of relying on GC (#36804). - web-tools(web_tools.py): include "xai" in check_web_api_key() so a configured X.AI web backend reports as available (#36802). - compression(conversation_compression.py): mark the feasibility check done only after it completes, and default the gate to "not checked" if the attribute is missing (#36803). - completion(completion.py): replace `ls` with directory globbing in the generated bash/zsh/fish profile listers — handles names with spaces and skips non-directory entries (#36806). - terminal-tool(terminal_tool.py): drop a duplicate `import threading` (#36808). - claw(claw.py): the migrate recommendation now points at the real `hermes gateway stop` command instead of the non-existent `hermes stop` (#36795, #36796, closes #36771). - tests: guard against a leaked HERMES_CRON_SESSION breaking gateway approval tests — add it to the hermetic conftest unset list (root cause, protects every test) and pop it in the affected test's setup_method (#36796). Co-authored-by: kyssta-exe <kyssta-exe@users.noreply.github.com>
2026-06-01 19:19:15 -07:00
"kyssta-exe@users.noreply.github.com": "kyssta-exe",
"shriganesh.patel@gmail.com": "ashishpatel26",
"45688690+fujinice@users.noreply.github.com": "fujinice",
"276689385+carltonawong@users.noreply.github.com": "carltonawong",
"195255660+EvilHumphrey@users.noreply.github.com": "EvilHumphrey",
"270604154+superearn-fisher@users.noreply.github.com": "superearn-fisher",
"3540493+kpadilha@users.noreply.github.com": "kpadilha",
"40378218+chaconne67@users.noreply.github.com": "chaconne67",
"Pluviobyte@users.noreply.github.com": "Pluviobyte",
"sanghyuk_seo@nexcubecorp.com": "sanghyuk-seo-nexcube",
"subrtt@gmail.com": "Brixyy",
"wangpuv@hotmail.com": "wangpuv",
"202622897+ticketclosed-wontfix@users.noreply.github.com": "ticketclosed-wontfix",
fix(codex-responses): gracefully recover from invalid_encrypted_content (salvage #10144) (#33035) * fix(codex-responses): gracefully recover from invalid_encrypted_content (salvage #10144) When an OpenAI-compatible Responses API surface accepts an initial request but later rejects the replayed `codex_reasoning_items` encrypted blob with HTTP 400 `invalid_encrypted_content`, the session previously got stuck retrying the same poisoned payload. Recovery: classify the error as a dedicated FailoverReason, and on the first hit disable encrypted reasoning replay for the rest of the session, strip cached items from message history, and retry once. Changes: * error_classifier: add FailoverReason.invalid_encrypted_content branch in _classify_400 (before context_overflow so the messages that mention 'encrypted content … could not be verified' don't trip context heuristics), in _classify_by_error_code, and extend _extract_error_code to peek inside wrapped JSON in error.message and ignore the bare '400' as a code. * agent_init: initialize `_codex_reasoning_replay_enabled = True` on every agent. * run_agent: add AIAgent._disable_codex_reasoning_replay() helper that flips the flag and pops cached items. * codex_responses_adapter: thread a `replay_encrypted_reasoning` kwarg through _chat_messages_to_responses_input so that when the flag is False we don't replay codex_reasoning_items. * transports/codex.py: read `replay_encrypted_reasoning` from params, thread it into the adapter, and gate the `include=['reasoning.encrypted_content']` request hint on it. * chat_completion_helpers: pass the agent's replay flag through to the transport. * conversation_loop: in the retry loop, add an invalid_encrypted_content recovery branch that fires once per session, only when api_mode == codex_responses, only when replay is still enabled, and only when at least one assistant message in history actually carries cached reasoning items (otherwise the 400 has nothing to do with our cache and the normal retry path handles it). Tests: * test_error_classifier: new wrapped-JSON _extract_error_code case; new TestClassifyApiError cases proving the 400 is retryable with no fallback, that the broad message match doesn't catch a generic 'parsed' message, and that the error code match is case-insensitive. * test_run_agent_codex_responses: end-to-end test of the recovery branch firing once and disabling replay, plus a sibling test that proves the branch does *not* fire (and the flag stays True) when history has no cached reasoning items. Salvages PR #10144 onto the post-refactor module layout (error_classifier / codex_responses_adapter / transports/codex / conversation_loop / agent_init) since the original diff was written against the pre-refactor monolithic run_agent.py. * chore(release): map victorGPT in AUTHOR_MAP for #10144 salvage --------- Co-authored-by: victorGPT <wuxuebin1993@gmail.com>
2026-05-26 22:01:17 -07:00
"wuxuebin1993@gmail.com": "victorGPT",
"xiaoxingitee@gmail.com": "xiaoxinova",
"wei.chen.coder@gmail.com": "wenchengxucool",
"frowte3k@gmail.com": "Frowtek",
"211828103+julio-cloudvisor@users.noreply.github.com": "julio-cloudvisor",
"17778+kweiner@users.noreply.github.com": "kweiner",
fix(kanban): bridge worker runtime activity to board heartbeat (#31752) The dispatcher watchdog (release_stale_claims) reads tasks.last_heartbeat_at to decide whether to reclaim a running task. The agent maintains its own in-process `_last_activity_ts` for every chunk/tool result, but those liveness ticks never reach the board unless the model explicitly calls the `kanban_heartbeat` tool — so a worker actively executing a long run without tool-level heartbeats can be reclaimed mid-flight as 'stale', returning the task to ready and orphaning the in-flight worker's progress. Fix: in `_touch_activity` (the canonical 'we just did work' hook in run_agent.py), call a new `heartbeat_current_worker_from_env` helper in `tools/kanban_tools.py` that: - No-ops outside dispatcher-spawned worker context (no HERMES_KANBAN_TASK). - Rate-limited to one DB write per 60s (runtime activity ticks too often to faithfully mirror; we just need the watchdog to see liveness). - Best-effort: never raises. heartbeat_claim + heartbeat_worker calls are individually try/except'd; any DB error logs at debug and returns. - Uses worker env identity: HERMES_KANBAN_TASK + HERMES_KANBAN_RUN_ID + HERMES_KANBAN_CLAIM_LOCK (all pinned by the dispatcher at spawn time). - No durable note on auto-heartbeats — that's reserved for the explicit `kanban_heartbeat` tool which carries a model-supplied note. The explicit `kanban_heartbeat` tool stays available unchanged for workers that want to attach a note or pre-emptively extend a claim across a known-long single tool call. Co-authored-by: faisfamilytravel <223516181+faisfamilytravel@users.noreply.github.com>
2026-05-28 23:25:42 -07:00
"223516181+faisfamilytravel@users.noreply.github.com": "faisfamilytravel",
fix(kanban): close three blocked/iteration-exhausted handling gaps (#29747) Reporter diagnosed three independent gaps that together allowed infinite 'unblock → re-stuck' loops with no surfacing or escalation: GAP 1: `_rule_stuck_in_blocked` resets timer on any `commented`/`unblocked` event, so a task that cycles every few minutes is invisible to it regardless of how many times it cycles. Fix: new `_rule_block_unblock_cycling` rule (`hermes_cli/kanban_diagnostics.py`) that counts block→unblock cycles in a sliding window. Default threshold 3 cycles within 24h, configurable via `block_cycle_threshold` / `block_cycle_window_seconds`. Walks events in arrival order (event id) since multiple events can share the same `created_at` second. Fires as a warning with a CLI hint to inspect the block reasons. GAP 2: Iteration-budget-exhausted runs in kanban workers map to `kanban_block` (status=blocked, but a clean exit from the kernel's perspective). `_rule_repeated_failures` reads `consecutive_failures`, which `_record_task_failure` increments only for crashed/timed_out/ spawn_failed — `blocked` outcome bypasses the failure counter, so the `kanban.failure_limit` circuit breaker never trips on budget-exhaustion loops. Fix: `agent/conversation_loop.py` budget-exhaustion path now calls `_record_task_failure(outcome="timed_out")` instead of `kanban_block`. Budget exhaustion is genuinely a timeout-shaped failure (the task ran out of allowed iterations), so this is more honest semantics; it also routes through the unified failure counter, so repeated budget exhaustions trip the circuit breaker and the task auto-blocks with `gave_up` after `failure_limit` retries. GAP 3: `release_stale_claims` uses `_pid_alive(worker_pid)` only and ignores `last_heartbeat_at`. Reporter observed a 91-min run that held its claim with frozen heartbeat because the worker entered a logic loop with no tool calls — `_pid_alive` kept returning True so the claim was extended every 15 minutes indefinitely. Fix: heartbeat-stale backstop. If `last_heartbeat_at` is set AND older than `DEFAULT_CLAIM_HEARTBEAT_MAX_STALE_SECONDS` (default 1h), reclaim even if the PID is alive. NULL `last_heartbeat_at` preserves backward compatibility (no heartbeat yet = extend, as before). The reclaim event payload now includes a `heartbeat_stale` boolean so operators see why a live-PID worker was reclaimed. This works cleanly in concert with PR #34418 (#31752 runtime → heartbeat bridge): once `_touch_activity` keeps `last_heartbeat_at` fresh as a side effect of normal API traffic, the backstop only fires for genuinely wedged workers (no chunks, no tool results, no progress at all). Co-authored-by: baofuen <45189813+baofuen@users.noreply.github.com>
2026-05-28 23:40:50 -07:00
"45189813+baofuen@users.noreply.github.com": "baofuen",
"interstellar.consulting@gmail.com": "Interstellar-code",
"33978413+Interstellar-code@users.noreply.github.com": "Interstellar-code",
"tillfalko@gmail.com": "tillfalko",
"hi@fesalfayed.com": "fesalfayed",
"marek.les@seznam.cz": "maxcz79",
# teknium (multiple emails)
"teknium1@gmail.com": "teknium1",
"kenyon1977@gmail.com": "kenyonxu",
"cipherframe@users.noreply.github.com": "CipherFrame",
"donovan-yohan@users.noreply.github.com": "donovan-yohan",
"121752779+jacevys@users.noreply.github.com": "jacevys",
fix(agent): consult supports_vision override in auto-mode routing The contributor PR (#17936) only patched the strip path in `_model_supports_vision()`. The auto-mode router in `agent/image_routing._lookup_supports_vision` still only read models.dev, so a custom-provider model declared as vision-capable would still get its images routed through vision_analyze in the default `agent.image_input_mode: auto` setting. Users had to set both `supports_vision: true` AND `image_input_mode: native` to bypass the text pipeline. Single-knob behavior now: `supports_vision: true` alone is enough in auto mode. The strip path and the routing path consult the same resolver. - Extract override resolution into `_supports_vision_override()` in agent/image_routing.py and wire it into `_lookup_supports_vision()`. - Refactor `run_agent._model_supports_vision` to call the same helper (DRY, single source of truth for the resolution order). - Strict YAML boolean coercion: `supports_vision: "false"` (quoted — a common YAML mistake) no longer coerces to True via bool() truthiness. Recognised tokens: true/false/yes/no/on/off/1/0 plus real bools and 0/1. Unrecognised values return None and fall through to models.dev. - Add @CNSeniorious000 to AUTHOR_MAP for release attribution. Tests: 26 new (TestCoerceCapabilityBool, TestSupportsVisionOverride, TestLookupSupportsVisionOverride, TestAutoModeRespectsOverride). Existing contributor tests + image_routing + vision_native_fast_path + native_image_buffer_isolation all green (92/92).
2026-05-20 22:59:14 -07:00
"me@promplate.dev": "CNSeniorious000",
"yichengqiao21@gmail.com": "YarrowQiao",
"erhanyasarx@gmail.com": "erhnysr",
"30366221+WorldWriter@users.noreply.github.com": "WorldWriter",
"dafeng@DafengdeMacBook-Pro.local": "WorldWriter",
"schepers.zander1@gmail.com": "Strontvod",
"ed@bebop.crew": "someaka",
"anadi.jaggia@gmail.com": "Jaggia",
"steve@steveonjava.com": "steveonjava",
"steveonjava@gmail.com": "steveonjava",
"squiddy@2rook.ai": "MoonRay305",
"annguyenNous@users.noreply.github.com": "annguyenNous",
"32201324+simpolism@users.noreply.github.com": "simpolism",
"simpolism@gmail.com": "simpolism",
"jake@nousresearch.com": "simpolism",
"mgongzai@gmail.com": "vKongv",
"0x.badfriend@gmail.com": "discodirector",
"altriatree@gmail.com": "TruaShamu",
"contact-me@stark-x.cn": "Stark-X",
feat(tui): mouse_tracking DEC mode presets (salvage of #26681) (#30084) * feat(tui): make display.mouse_tracking pick which DEC modes to enable Previously the boolean flag was all-or-nothing across modes 1000+1002+1003+1006. Inside tmux, mode 1003 (any-motion) makes every mouse cross of the prompt row fire a clipboard probe that surfaces as "No image in clipboard" — sometimes dozens in a row. Disabling tracking entirely killed scroll-wheel scrolling too, since tmux's own scrollback is preempted by the alt-screen TUI. `display.mouse_tracking` (and `/mouse <preset>`) now accepts `off | wheel | buttons | all` in addition to the legacy booleans. `wheel` is 1000+1006: scroll wheel + click only, no drag, no hover — the tmux-friendly subset. `buttons` adds 1002 for drag-to-select. `all` (= legacy `true`) keeps the hover-driven UI (scrollbar paginate-on-hover, link mouseenter, etc.). * fix(tui): repaint + sync mouse mode when display.mouse_tracking changes Two interacting bugs left the TUI blank when `display.mouse_tracking` switched at runtime (config edit, /mouse <preset>): 1. AlternateScreen's effect re-runs on every `mouseTracking` change, tearing down and re-entering the alt screen. After re-entry, ink's frame buffers are reset by `resetFramesForAltScreen()` but nothing schedules the follow-up render — the alt screen sits blank until some other state change happens to trigger one. Add a `scheduleRender()` in `setAltScreenActive`'s active=true branch so the freshly-entered alt screen gets a full repaint immediately. 2. `setAltScreenActive` early-returns when `active` hasn't changed, which silently drops a `mouseTracking` change if the cleanup→setup pair somehow leaves `altScreenActive` already true. Call `setAltScreenMouseTracking` explicitly from the AlternateScreen effect so the in-memory mode and terminal DECSET sequence stay in sync regardless of how `setAltScreenActive` resolved (the call is a no-op when the mode is unchanged). * fix(tui): address copilot review #4341269705 - tui_gateway/server.py: drop the never-referenced _MOUSE_TRACKING_MODES frozenset (comment #3284802434). _MOUSE_TRACKING_ALIASES already centralizes the canonical preset set via its values; the separate constant added no behavior. - tests/test_tui_gateway_server.py: update the existing test_config_mouse_uses_documented_key_with_legacy_fallback to assert the new preset strings ('all'/'off' instead of 'on'/'off', display.mouse_tracking persisted as 'all' instead of True) and add test_config_mouse_accepts_preset_strings_and_aliases covering /mouse set with wheel/click/unknown (comment #3284802453). The on/off legacy config.set return shape was an implementation detail of the boolean flag, not a stable API — the slash command, gateway help text, and docs all advertise the preset values now. - ui-tui/packages/hermes-ink/src/ink/ink.tsx: schedule a render at the end of reenterAltScreen() (comment #3284802461). Mirrors the same fix in setAltScreenActive() from ece0a2f4c — without it, SIGCONT/resize self-heal/stdin-gap re-entry leaves the alt screen blank because every caller returns early after invoking us. * fix(tui): address copilot review #4341308478 round 2 - ui-tui/src/config/env.ts (comment #3284837577): the precedence comment was misleading. Actual behavior on origin/main is HERMES_TUI_MOUSE_TRACKING (explicit override) > Termux default > HERMES_TUI_DISABLE_MOUSE legacy kill-switch. This is preserved from main; the only change here was the wrong comment that claimed DISABLE_MOUSE kept kill-switch semantics. Rewrote the comment block to document the actual precedence ladder. - tui_gateway/server.py /mouse set (comment #3284837607): replaced 'str(value or "").strip().lower()' with the explicit None idiom already used for /indicator, so programmatic callers can pass 0 / False and have them route through _MOUSE_TRACKING_ALIASES → 'off' instead of collapsing to '' and triggering the toggle path. - ui-tui/packages/hermes-ink/src/ink/components/AlternateScreen.tsx (comment #3284837620): always prepend DISABLE_MOUSE_TRACKING before enableMouseTrackingFor(...) on mount. Otherwise selecting 'wheel'/'buttons' from a state where DEC 1003 was already asserted (crash, another app, debugger) would silently leave hover on. Also unconditionally DISABLE on unmount so a crash mid-mount can't leak DEC modes back to the host shell. * chore(release): map nat@nthrow.io to @nthrow for #26681 salvage * fix(tui): drop redundant setAltScreenMouseTracking in AlternateScreen Copilot review #4341356637 (comment #3284880417). The explicit setAltScreenMouseTracking(mouseTracking) after setAltScreenActive(true, mouseTracking) was defensive paranoia added in the previous fix commit that's not actually reachable in practice: - React's cleanup always runs before the next setup, so on any prop change (mouseTracking or writeRaw) the cleanup sets active=false first. Setup then sees active was false and applies the new mode via setAltScreenActive without early-returning. - On the impossible 'active stayed true' path, the writeRaw above has already sent DISABLE_MOUSE_TRACKING + enableMouseTrackingFor(newMode) to the terminal, so the in-memory mode would lag but the visible state is already correct. Removing the redundant call means a single DEC sequence per mount. If the 'active stayed true' path ever manifests in practice, the right fix is in setAltScreenActive (track mode regardless of the active early-return), not here. * fix(tui): always DISABLE before enableMouseTrackingFor in ink.tsx Copilot review #4341379994 (comments #3284900825, #3284900840, #3284900852). Three remaining call sites in ink.tsx still re-enabled mouse tracking without first sending DISABLE_MOUSE_TRACKING: - handleResize alt-screen recovery (line ~577) - reassertTerminalModes stdin-gap re-assertion (line ~1351) - reenterAltScreen SIGCONT/resize/stdin-gap self-heal (line ~1408) For 'wheel'/'buttons' presets, omitting DISABLE leaves any externally- asserted DEC 1003 (other apps, prior crash, tmux state) still active and the hover-free preset silently has hover on. DISABLE_MOUSE_TRACKING is idempotent and safe to send unconditionally — it resets all four modes. Matches the pattern already in setAltScreenMouseTracking and the AlternateScreen mount path. * fix(tui): always DISABLE before enableMouseTrackingFor in exitAlternateScreen Copilot review #4341452823 (comment #3284959762). exitAlternateScreen() was the last call site in ink.tsx still re-enabling mouse tracking without DISABLE first. Editors (vim/nvim/less) and tmux can leave DEC 1003 hover asserted across the handoff back; without DISABLE, 'wheel'/'buttons' presets silently kept hover on after the editor quit. Now all five enableMouseTrackingFor() call sites in ink.tsx prepend DISABLE_MOUSE_TRACKING — handleResize, reassertTerminalModes, reenterAltScreen, setAltScreenMouseTracking, exitAlternateScreen. * fix(tui): add defensive default to enableMouseTrackingFor switch Copilot review #4341485231 (comment #3284979323). TS exhaustive switch returns string per the type system, but a JS caller / corrupted config / hot-reload-in-dev could reach the function with an unknown value at runtime. Without a default, that path returns undefined which then concatenates as the literal string 'undefined' into the terminal byte stream — visibly garbling output. Treat unknown as 'off' (no DEC sequences) so the worst case is silent input loss rather than a wrecked screen. --------- Co-authored-by: Nat Thrower <nat@nthrow.io>
2026-05-21 20:25:52 -05:00
"nat@nthrow.io": "nthrow",
"m@mobrienv.dev": "mikeyobrien",
"saeed919@pm.me": "falasi",
"chrisdlc119@outlook.com": "chdlc",
2026-05-20 22:57:25 -07:00
"omar@techdeveloper.site": "nycomar",
"qiyin.zuo@pcitc.com": "qiyin-code",
"mr.aashiz@gmail.com": "aashizpoudel",
"adityargadgil@gmail.com": "AdityaRajeshGadgil",
"70629228+shaun0927@users.noreply.github.com": "shaun0927",
"soju06@users.noreply.github.com": "Soju06",
"34199905+Soju06@users.noreply.github.com": "Soju06",
"98262967+Bihruze@users.noreply.github.com": "Bihruze",
"189280367+Lempkey@users.noreply.github.com": "Lempkey",
"34853915+m0n3r0@users.noreply.github.com": "m0n3r0",
"leeseoki@makestar.com": "leeseoki0",
"kronexoi13@gmail.com": "kronexoi",
"hua.zhong@kingsmith.com": "vgocoder",
"hermes@marian.local": "Schrotti77",
"david@memorilabs.ai": "devwdave",
"dave@devwdave.com": "devwdave",
"1920071390@campus.ouj.ac.jp": "zapabob",
"zapabob@users.noreply.github.com": "zapabob",
"gaia@gaia.local": "jfuenmayor",
"jiahuigu@users.noreply.github.com": "Jiahui-Gu",
"openhands@all-hands.dev": "YLChen-007",
feat(skills): add optional openhands skill — closes #477 Adds an optional autonomous-ai-agents skill that delegates coding tasks to the OpenHands CLI (https://github.com/All-Hands-AI/OpenHands). Sits alongside claude-code / codex / opencode and is the model-agnostic option in that family — any LiteLLM-supported provider works. This is a ground-truth rewrite of #19325 by @xzessmedia (Tim Koepsel). The original PR's SKILL.md was drafted by the OpenHands agent itself and hallucinated several flags that don't exist in the real CLI (\`--model\`, \`--max-iterations\`, \`--workspace\`, \`--sandbox docker\`), pointed at the wrong PyPI package (\`openhands-ai\`, which is the legacy V0 SDK), and claimed native Windows support that the upstream docs explicitly disclaim. Rather than cherry-pick and rewrite half the lines under contributor authorship, the SKILL.md was rebuilt against a verified install (\`uv tool install openhands --python 3.12\`) and a real end-to-end \`--headless --json\` run against openrouter/openai/gpt-4o-mini. Authorship credited via the \`author:\` frontmatter field and an AUTHOR_MAP entry in scripts/release.py. Changes: - optional-skills/autonomous-ai-agents/openhands/SKILL.md (new) - website/docs/user-guide/skills/optional/autonomous-ai-agents/autonomous-ai-agents-openhands.md (auto-gen) - website/docs/reference/optional-skills-catalog.md (one new row) - website/sidebars.ts (one new entry under Optional → Autonomous AI Agents) - scripts/release.py (AUTHOR_MAP entry for xzessmedia) Pitfalls documented in the SKILL came from running the tool, not from the upstream README: LiteLLM bedrock/sagemaker stderr noise on every invocation, banner spam (\`OPENHANDS_SUPPRESS_BANNER=1\` required), \`--override-with-envs\` mandatory or the CLI ignores LLM_* env vars entirely, the dashed-vs-undashed Conversation ID footgun for \`--resume\`, LiteLLM model-slug double-prefix when going through OpenRouter.
2026-05-25 14:32:34 -07:00
"3153586+xzessmedia@users.noreply.github.com": "xzessmedia",
"AdamPlatin123@outlook.com": "AdamPlatin123",
"32711803+waefrebeorn@users.noreply.github.com": "waefrebeorn",
"32869278+dusterbloom@users.noreply.github.com": "dusterbloom",
"189737461+basilalshukaili@users.noreply.github.com": "basilalshukaili",
"liuhao1024@users.noreply.github.com": "liuhao1024",
"annguyenNous@users.noreply.github.com": "annguyenNous",
"285874597+annguyenNous@users.noreply.github.com": "annguyenNous",
"kylekahraman@users.noreply.github.com": "kylekahraman",
"130975919+kylekahraman@users.noreply.github.com": "kylekahraman",
"seppe@fushia.be": "seppegadeyne",
"18264851+seppegadeyne@users.noreply.github.com": "seppegadeyne",
"blackpilledsoftware@gmail.com": "blackpilledsoftware-prog",
"266800570+blackpilledsoftware-prog@users.noreply.github.com": "blackpilledsoftware-prog",
"dsr-restyn@users.noreply.github.com": "dsr-restyn",
"210765158+WuKongAI-CMU@users.noreply.github.com": "WuKongAI-CMU",
"lichriszhang@gmail.com": "codeblackhole1024",
"leovillalbajr@gmail.com": "Lempkey",
"nidhi2894@gmail.com": "nidhi-singh02",
"30312689+aashizpoudel@users.noreply.github.com": "aashizpoudel",
2026-05-05 17:02:01 -07:00
"oleksii.lisikh@gmail.com": "olisikh",
fix(async): close unscheduled coroutines in all threadsafe bridges (#26584) Wraps every sync->async coroutine-scheduling site in the codebase with a new agent.async_utils.safe_schedule_threadsafe() helper that closes the coroutine on scheduling failure (closed loop, shutdown race, etc.) instead of leaking it as 'coroutine was never awaited' RuntimeWarnings plus reference leaks. 22 production call sites migrated across the codebase: - acp_adapter/events.py, acp_adapter/permissions.py - agent/lsp/manager.py - cron/scheduler.py (media + text delivery paths) - gateway/platforms/feishu.py (5 sites, via existing _submit_on_loop helper which now delegates to safe_schedule_threadsafe) - gateway/run.py (10 sites: telegram rename, agent:step hook, status callback, interim+bg-review, clarify send, exec-approval button+text, temp-bubble cleanup, channel-directory refresh) - plugins/memory/hindsight, plugins/platforms/google_chat - tools/browser_supervisor.py (3), browser_cdp_tool.py, computer_use/cua_backend.py, slash_confirm.py - tools/environments/modal.py (_AsyncWorker) - tools/mcp_tool.py (2 + 8 _run_on_mcp_loop callers converted to factory-style so the coroutine is never constructed on a dead loop) - tui_gateway/ws.py Tests: new tests/agent/test_async_utils.py covers helper behavior under live loop, dead loop, None loop, and scheduling exceptions. Regression tests added at three PR-original sites (acp events, acp permissions, mcp loop runner) mirroring contributor's intent. Live-tested end-to-end: - Helper stress test: 1500 schedules across live/dead/race scenarios, zero leaked coroutines - Race exercised: 5000 schedules with loop killed mid-flight, 100 ok / 4900 None returns, zero leaks - hermes chat -q with terminal tool call (exercises step_callback bridge) - MCP probe against failing subprocess servers + factory path - Real gateway daemon boot + SIGINT shutdown across multiple platform adapter inits - WSTransport 100 live + 50 dead-loop writes - Cron delivery path live + dead loop Salvages PR #2657 — adopts contributor's intent over a much wider site list and a single centralized helper instead of inline try/except at each site. 3 of the original PR's 6 sites no longer exist on main (environments/patches.py deleted, DingTalk refactored to native async); the equivalent fix lives in tools/environments/modal.py instead. Co-authored-by: JithendraNara <jithendranaidunara@gmail.com>
2026-05-15 14:00:01 -07:00
"jithendranaidunara@gmail.com": "JithendraNara",
"islam666@users.noreply.github.com": "islam666",
"30467832+islam666@users.noreply.github.com": "islam666",
"jeremy@geocaching.com": "outdoorsea",
"54763683+thedavidmurray@users.noreply.github.com": "thedavidmurray",
"leone.parise@gmail.com": "leoneparise",
"mr@shu.io": "mrshu",
"adam.manning@gmail.com": "am423",
"buraysandro9@gmail.com": "ygd58",
"108427749+buntingszn@users.noreply.github.com": "buntingszn",
"yanglongwei06@gmail.com": "Alex-yang00",
"yanghongda@jackyun.com": "yangguangjin",
"teknium@nousresearch.com": "teknium1",
2026-05-20 22:10:44 -07:00
"markuscontasul@gmail.com": "Glucksberg",
"80581902+Glucksberg@users.noreply.github.com": "Glucksberg",
"piyushvp1@gmail.com": "thelumiereguy",
"pnascimento9596@gmail.com": "pnascimento9596",
"dskwelmcy@163.com": "dskwe",
"421774554@qq.com": "wuli666",
fix(deepseek): wire thinking-mode via DeepSeekProfile, not legacy fallback The cherry-picked PR #15251 from @tw2818 correctly identified the DeepSeek 400 root cause but placed the fix in the legacy fallback path of `build_kwargs`, which DeepSeek never reaches — DeepSeek has a registered ProviderProfile and goes through `_build_kwargs_from_profile` instead. The legacy-path block was therefore dead code. This commit pivots the fix to where it actually fires: - New `DeepSeekProfile` in `plugins/model-providers/deepseek/__init__.py` overrides `build_api_kwargs_extras` to emit DeepSeek's expected wire format (mirrors `KimiProfile`): {"reasoning_effort": "<low|medium|high|max>", "extra_body": {"thinking": {"type": "enabled" | "disabled"}}} - Model gating: only `deepseek-v4-*` and `deepseek-reasoner` emit thinking control. `deepseek-chat` (V3) is untouched — current behavior. - Effort mapping: low/medium/high passthrough, xhigh/max → max, unset → omitted (DeepSeek server applies its own default). - Revert the legacy-path additions from PR #15251 — they were dead code, and the `_copy_reasoning_content_for_api` strip block specifically would have nullified the existing reasoning_content padding machinery (`_needs_deepseek_tool_reasoning` → space-pad on replay) that the active provider already relies on for replay correctness. - Unit tests pin the wire-shape contract and the model gating rules (26 tests, all passing). Existing transport + provider profile suites (321 tests) continue to pass. - AUTHOR_MAP: map twebefy@gmail.com → tw2818 for release notes credit. Closes #15700, #17212, #17825. Co-authored-by: tw2818 <twebefy@gmail.com>
2026-05-15 16:39:18 -07:00
"twebefy@gmail.com": "tw2818",
"harish.kukreja@gmail.com": "counterposition",
fix(deps): pin brotlicffi so aiohttp can decode Discord's Brotli attachments Discord's CDN serves attachments with Content-Encoding: br. aiohttp's compression_utils tries 'import brotlicffi as brotli' first and falls back to google's Brotli, but Brotli<1.2.0's Decompressor.process() is 1-arg while aiohttp calls it with 2 args (data, max_length). Result: every .txt/.md/.doc uploaded to a Discord-gateway session fails to decode at att.read() with 'Can not decode content-encoding: br' / 'TypeError: process() takes exactly 1 argument (2 given)', the agent never sees the bytes, and falls back to filesystem guessing. Pin brotlicffi==1.2.0.1 in both surfaces: - tools/lazy_deps.py 'platform.discord' tuple: Discord users on the lazy-install path get it on first discord.py import. - pyproject.toml [messaging] extra: users who explicitly install hermes-agent[messaging] (skipping the lazy path) get it eagerly. brotlicffi wins aiohttp's import race regardless of what else is installed (try brotlicffi / except: import brotli), so existing setups that already pulled google's Brotli transitively don't change behavior beyond the bug fix. ~1.5 MB wheel, manylinux/macOS/Windows coverage. E2E verified: round-trip decode of Brotli-compressed payload via aiohttp.compression_utils.brotli succeeds with brotlicffi pinned; same test against Brotli==1.1.0 alone reproduces the reported TypeError. Credit to @Korkyzer for the original diagnosis and fix shape in #15744; the lazy-deps gating layer was added on top to keep brotlicffi out of the install path for users who don't run a Discord gateway. Fixes #12511. Closes #15744. Co-authored-by: Korky <korkyzer@gmail.com>
2026-05-14 22:30:12 -07:00
"korkyzer@gmail.com": "Korkyzer",
"1046611633@qq.com": "zhengyn0001",
"1095245867@qq.com": "littlewwwhite",
"db@project-aeon.com": "db-aeon",
"ahmed@abadr.net": "ahmedbadr3",
"63822243+CoinTheHat@users.noreply.github.com": "CoinTheHat",
"cleo@edaphic.xyz": "curiouscleo",
"hirokazu.ogawa@kwansei.ac.jp": "hrkzogw",
"datapod.k@gmail.com": "dandacompany",
"treydong.zh@gmail.com": "TreyDong",
"phil.thomas@gametime.co": "explainanalyze",
"kyanam.preetham@gmail.com": "pkyanam",
"zhizhong.xu@shopee.com": "1000Delta",
"30397170+1000Delta@users.noreply.github.com": "1000Delta",
fix(cli): clamp scrollback box widths + suppress status bar after resize (#25975) When the terminal shrinks, already-printed box-drawing rules (response, reasoning, streaming TTS, background-task Panels) reflow into multiple narrower rows — visible as duplicated horizontal separators / ghost lines in scrollback. Similarly, prompt_toolkit redraws a fresh status bar on SIGWINCH on top of one the terminal just reflowed, producing double-bar artifacts on column shrink. Two surgical changes: 1. Decorative scrollback boxes now use a new `HermesCLI._scrollback_box_width()` helper that clamps to `max(32, min(width, 56))`. The live TUI footer is unaffected and still uses the full width. Covers: streaming response box (open + close), reasoning box (open + close, both streaming and post-stream paths), streaming-TTS box close, final-response Rich Panel, and the background-task Rich Panel. 2. `_recover_after_resize()` now also sets a new `_status_bar_suppressed_after_resize` flag so the dynamic status bar and both input separator rules stay hidden until the next user input. The flag is cleared in the process loop the moment the user submits their next prompt, restoring chrome cleanly. Tests: - New `test_input_rules_hide_after_resize_until_next_input` covers the flag's effect on rule heights. - New `test_scrollback_box_width_caps_to_resize_safe_value` covers the helper at floor / cap / mid-range / overflow. - Existing resize-recovery test extended to assert the flag flips. Refs: #18449 #19280 #22976 Salvage of #24403. Co-authored-by: Szymonclawd <szymonclawd@mac.home>
2026-05-14 15:22:44 -07:00
"szymonclawd@mac.home": "szymonclawd",
"257759490+szymonclawd@users.noreply.github.com": "szymonclawd",
"101180447+worlldz@users.noreply.github.com": "worlldz",
"zhanganzhe@tenclass.com": "luoyuctl",
"51604064+luoyuctl@users.noreply.github.com": "luoyuctl",
"127238744+teknium1@users.noreply.github.com": "teknium1",
"tolle.lege+github@gmail.com": "InB4DevOps",
"73686890+InB4DevOps@users.noreply.github.com": "InB4DevOps",
"147827411+EloquentBrush@users.noreply.github.com": "AhmetArif0",
"97489706+purzbeats@users.noreply.github.com": "purzbeats",
"hugosequier@gmail.com": "Hugo-SEQUIER",
"kylejeong21@gmail.com": "Kylejeong2",
"128259593+Gutslabs@users.noreply.github.com": "Gutslabs",
fix(mcp-oauth): persist OAuth server metadata across process restarts (#21226) The MCP SDK discovers OAuth server metadata (token_endpoint, etc.) on demand and keeps it in memory only. Without disk persistence, a restart with valid cached refresh tokens forces the SDK to fall back to the guessed '{server_url}/token' path — which returns 404 on most real providers (Notion, Atlassian, GitHub remote MCP, etc.) and triggers a full browser re-authorization even though the refresh token is fine. Add a .meta.json file next to the existing tokens/client_info files: HERMES_HOME/mcp-tokens/<server>.json -- tokens (existing) HERMES_HOME/mcp-tokens/<server>.client.json -- client info (existing) HERMES_HOME/mcp-tokens/<server>.meta.json -- oauth metadata (new) Changes: - HermesTokenStorage.save_oauth_metadata / load_oauth_metadata / _meta_path — disk layer for the discovered OAuthMetadata. - HermesTokenStorage.remove() now also clears .meta.json so 'hermes mcp remove <name>' and the manager's remove() path clean up fully. - HermesMCPOAuthProvider._initialize cold-restores from disk before the existing pre-flight discovery runs. If disk has metadata we skip the discovery HTTP round-trips entirely. - HermesMCPOAuthProvider._prefetch_oauth_metadata now persists ASM as soon as it's discovered, so even the first pre-flight run seeds disk. - HermesMCPOAuthProvider._persist_oauth_metadata_if_changed() is called at the end of async_auth_flow so metadata discovered via the SDK's lazy 401-branch (not pre-flight) is also saved for next time. Tests cover the storage roundtrip (save/load/missing/corrupt/remove) and the manager provider path (cold-load restore, skip-when-in-memory, persist-on-discover, noop-when-unchanged, end-to-end async_auth_flow). Co-authored-by: nocturnum91 <50326054+nocturnum91@users.noreply.github.com>
2026-05-07 05:35:33 -07:00
"50326054+nocturnum91@users.noreply.github.com": "nocturnum91",
"52470719+gianfrancopiana@users.noreply.github.com": "gianfrancopiana",
feat(web): add Brave Search (free tier) and DDGS search providers Both implement WebSearchProvider via tools/web_providers/ — matching the existing SearXNG pattern (PR #5c906d702). Search-only; pair with any extract provider via web.extract_backend. - tools/web_providers/brave_free.py — Brave Search API (free tier, 2k queries/mo). Uses BRAVE_SEARCH_API_KEY as X-Subscription-Token. - tools/web_providers/ddgs.py — DuckDuckGo via the ddgs Python package. No API key; gated on package importability. - tools/web_tools.py: both backends added to _get_backend() config list and auto-detect chain (trails paid providers), _is_backend_available, web_search_tool dispatch, web_extract_tool + web_crawl_tool search-only refusals, check_web_api_key, and the __main__ diagnostic. Introduces _ddgs_package_importable() helper so tests can monkeypatch a single symbol for the ddgs availability check. - hermes_cli/tools_config.py: picker entries for both providers; ddgs gets a post_setup handler that runs `pip install ddgs`. - hermes_cli/config.py: BRAVE_SEARCH_API_KEY in OPTIONAL_ENV_VARS. - scripts/release.py: AUTHOR_MAP entry for @Abd0r. - tests: 14 new tests (brave-free) + 15 new tests (ddgs) covering provider unit behavior, backend wiring, and search-only refusals. Salvages the brave-free + ddgs portion of PR #19796. Not included: the in-line helpers in web_tools.py (replaced with provider modules to match the shipped architecture), the lynx-based extract path (these backends should refuse extract with a clear error — users pair with a real extract provider), and scripts/start-llama-server.sh (unrelated). Co-authored-by: Abd0r <223003280+Abd0r@users.noreply.github.com>
2026-05-07 07:23:03 -07:00
"223003280+Abd0r@users.noreply.github.com": "Abd0r",
"HuangYuChuh@users.noreply.github.com": "HuangYuChuh",
"aaronwong1989@gmail.com": "hrygo",
"26729613+hrygo@users.noreply.github.com": "hrygo",
"erenkar950@gmail.com": "eren-karakus0",
"aubrey@freeman-wisco.com": "Freeman-Consulting",
2026-05-10 16:25:33 -07:00
"don.rhm@gmail.com": "rahimsais",
"40222899+rahimsais@users.noreply.github.com": "rahimsais",
2026-05-10 19:12:52 -07:00
"alfred@Alfreds-Mac-mini.local": "NivOO5",
"231191380+NivOO5@users.noreply.github.com": "NivOO5",
"jameshuang@gmail.com": "kjames2001",
"62420081+kjames2001@users.noreply.github.com": "kjames2001",
2026-05-10 22:18:14 -07:00
"132184373+wilsen0@users.noreply.github.com": "wilsen0",
"ra2157218@gmail.com": "Abd0r",
"oswaldb22@users.noreply.github.com": "oswaldb22",
"abdielv@proton.me": "AJV20",
"mason@growagainorchids.com": "masonjames",
"108541149+amethystani@users.noreply.github.com": "amethystani",
"ytchen0719@gmail.com": "liquidchen",
"am@studio1.tailb672fe.ts.net": "subtract0",
feat(gateway): per-platform admin/user split for slash commands (salvage of #4443) (#23373) * feat(gateway): per-platform admin/user split for slash commands Adds an opt-in two-list access control on top of the existing per-platform `allow_from` allowlists, scoped to slash commands only: - allow_admin_from — full slash command access - user_allowed_commands — what non-admins may run - group_allow_admin_from — same, group/channel scope - group_user_allowed_commands When `allow_admin_from` is unset for a scope, gating is disabled and every allowed user keeps full access (backward compat). Plain chat is unaffected. `/help` and `/whoami` are always reachable so users can see what they can run. Gate runs at the slash command dispatch site in gateway/run.py and uses `is_gateway_known_command()`, so it covers built-in AND plugin-registered commands through the live registry without per-feature wiring. Adds `/whoami` showing platform, scope, tier, and runnable commands. Salvage of PR #4443's permission tier work, scoped down. The full tier system, tool filtering, audit log, usage tracking, rate limiting, `/promote` flow, and persistent SQLite stores are not included here — those can be re-expanded later if needed. Co-authored-by: ReqX <mike@grossmann.at> * fix(gateway): close running-agent fast-path bypass + add coverage and central docs The slash command access gate was only applied at the cold dispatch site (line ~5921). When an agent was already running, the running-agent fast-path block (line ~5574) dispatched /restart, /stop, /new, /steer, /model, /approve, /deny, /agents, /background, /kanban, /goal, /yolo, /verbose, /footer, /help, /commands, /profile, /update directly without going through the gate — letting non-admins bypass gating just because an agent happens to be busy. Refactored the gate into _check_slash_access() and called from BOTH paths. /status remains intentionally pre-gate so users can always see session state. Also added 18 more dispatch tests covering: - Running-agent fast-path: blocks non-admin, allows admin, /status always works - Alias canonicalization (gate uses canonical name, not user alias) - Unknown / unregistered commands pass through (don't false-positive) - DM admin scope-locked when group has its own admin list - Multi-platform isolation (Discord gated, Telegram unrestricted) Docs: added Slash Command Access Control section to the central messaging index page + /whoami row in the chat commands table. Co-authored-by: ReqX <mike@grossmann.at> --------- Co-authored-by: ReqX <mike@grossmann.at>
2026-05-10 12:33:54 -07:00
"mike@grossmann.at": "ReqX",
"axmaiqiu@gmail.com": "qWaitCrypto",
2026-05-09 14:48:13 -07:00
"44045911+kidonng@users.noreply.github.com": "kidonng",
2026-05-09 14:48:54 -07:00
"daniellsmarta@gmail.com": "DanielLSM",
2026-05-09 14:49:36 -07:00
"264291321+v1b3coder@users.noreply.github.com": "v1b3coder",
2026-05-09 14:50:53 -07:00
"silverchris@foxmail.com": "ming1523",
2026-05-09 13:14:03 -07:00
"maksesipov@gmail.com": "Qwinty",
"byquenox@gmail.com": "Que0x",
2026-05-09 13:14:46 -07:00
"denisamania@gmail.com": "CalmProton",
2026-05-09 13:19:56 -07:00
"308068+mbac@users.noreply.github.com": "mbac",
2026-05-12 01:45:40 +05:30
"nicoechaniz@altermundi.net": "nicoechaniz",
2026-05-09 13:23:39 -07:00
"ninso112@proton.me": "Ninso112",
"wesleysimplicio@live.com": "wesleysimplicio",
2026-05-09 09:00:24 -07:00
"matthew.dean.cater@gmail.com": "SiliconID",
2026-05-09 11:09:50 -07:00
"xieniu@proton.me": "xieNniu",
2026-05-09 09:08:17 -07:00
"rw8143a@american.edu": "wali-reheman",
"egitimviscara@gmail.com": "uzunkuyruk",
"zhekinmaksim@gmail.com": "Zhekinmaksim",
"obafemiferanmi1999@gmail.com": "KvnGz",
2026-05-03 01:44:17 -07:00
"159539633+MottledShadow@users.noreply.github.com": "MottledShadow",
"aludwin+gh@gmail.com": "adamludwin",
"ngusev@astralinux.ru": "NikolayGusev-astra",
"liuguangyong201@hellobike.com": "liuguangyong93",
"2093036+exiao@users.noreply.github.com": "exiao",
"20nik.nosov21@gmail.com": "nik1t7n",
"thunderggnn@gmail.com": "ggnnggez",
"haozhe4547@gmail.com": "ehz0ah",
"eloklam2002@gmail.com": "eloklam",
refactor(gateway): simplify auto-resume + extend to crash recovery Follow-up on top of @kyan12's PR #20888 — same feature, cleaner shape, wider coverage. Changes: - Drop the synthetic '[System note: ...]' in the internal MessageEvent. The existing _is_resume_pending branch in _handle_message_with_agent (run.py ~L13738) already injects a reason-aware recovery system note on the next turn. With kyan's text in place the model saw two stacked system notes. Now the event text is empty and the existing injection path owns the wording. - Drop SessionStore.list_resume_pending() as a new public method. The filter is 8 lines inline in _schedule_resume_pending_sessions() — one caller, no other pluggability need. - Add 'restart_interrupted' to the auto-resume reason set. That's the reason SessionStore.suspend_recently_active() stamps on sessions recovered from a crash/OOM/SIGKILL (no .clean_shutdown marker). Previously those sessions had to wait for a real user message to auto-resume; now they continue automatically at startup like drain-timeout interruptions do. - Reasons live in a _AUTO_RESUME_REASONS frozenset at class scope so future reasons (e.g. 'manual_resume_request') can be opted in with one line. Test coverage added: - drain-timeout + crash-recovery both scheduled - stale entries skipped (outside freshness window) - suspended entries skipped (suspended > resume_pending) - originless entries skipped (no routing target) - disallowed reasons skipped (graceful forward-compat) E2E verified end-to-end with a real on-disk SessionStore: 2 eligible sessions scheduled, 2 ineligible skipped, empty-text internal events delivered to the adapter. Co-authored-by: Kevin Yan <kevyan1998@gmail.com>
2026-05-07 05:03:16 -07:00
"kevyan1998@gmail.com": "kyan12",
"rylen.anil@gmail.com": "rylena",
"godnanijatin@gmail.com": "jatingodnani",
"252811164+adybag14-cyber@users.noreply.github.com": "adybag14-cyber",
"14046872+tmimmanuel@users.noreply.github.com": "tmimmanuel",
"112875006+donramon77@users.noreply.github.com": "donramon77",
"657290301@qq.com": "IMHaoyan",
"revar@users.noreply.github.com": "revaraver",
"dengtaoyuan@dengtaoyuandeMac-mini.local": "dengtaoyuan450-a11y",
2026-05-05 08:35:59 -07:00
"ysfalweshcan@gmail.com": "Junass1",
2026-05-05 08:37:09 -07:00
"bartokmagic@proton.me": "Bartok9",
"bartok9@users.noreply.github.com": "Bartok9",
"erhanyasarx@gmail.com": "erhnysr", # PR #25198 salvage (tool-progress flood-control)
"cryptobyz.airdrop@gmail.com": "CryptoByz", # PR #25630 salvage (polling conflict Stage 1+2)
"fabioxxx@gmail.com": "fabiosiqueira", # PR #27212 salvage (bg-process notif anchor)
"lordfalcon.exe@gmail.com": "falconexe", # PR #24511 salvage (sticky-IP reset)
"fonhal@gmail.com": "fonhal", # PR #27865/#27861 salvage (mention entities / typing fallback)
"zyrixtrex@gmail.com": "Zyrixtrex", # PR #26754 salvage (avoid duplicate text after auto-TTS)
"264138787+nftpoetrist@users.noreply.github.com": "nftpoetrist", # PR #25856 salvage (escape slash-confirm preview)
"197455947+samahn0601@users.noreply.github.com": "samahn0601", # PR #27887 salvage (retry wrapped connect timeouts)
"gonzes7@gmail.com": "aqilaziz", # PR #26406 salvage (preserve native audio outside Telegram)
"karthikeyann@users.noreply.github.com": "karthikeyann", # PR #26609 salvage (DM-topic routing pin)
"rino.alpin@gmail.com": "kunci115", # PR #27098 salvage (thread-not-found retry)
"hayka-pacha@users.noreply.github.com": "hayka-pacha", # PR #25270 salvage (registry-aware mcp_ prefix strip)
"237601532+chromalinx@users.noreply.github.com": "chromalinx", # PR #27014 salvage (commands for groups+DM)
"booker1207@gmail.com": "booker1207", # PR #25132 salvage (gate profile bots by allowed topics)
"kiranvk2011@gmail.com": "kiranvk-2011", # PR #24815 salvage (image documents → vision)
"kosmonaut-t@centrum.cz": "rak135", # PR #25960 salvage (Windows /restart)
"bot.chi.online@gmail.com": "B0Tch1", # PR #27634 salvage (disable_topic_auto_rename)
"1037461232@qq.com": "jackjin1997", # PR #27239 salvage (restore DM topic thread_id after split)
"soynchuux@gmail.com": "soynchux", # PR #27806 salvage (chat-scoped auth without user_id)
"psikonetik@gmail.com": "el-analista", # PR #25368 salvage (cron topic fallback report)
"75435655+khungate@users.noreply.github.com": "khungate", # PR #25829 salvage (gmail-triage gt: callbacks)
"stevehq26-bot@users.noreply.github.com": "stevehq26-bot", # PR #28015 salvage (quick-command-only menus)
"seaverb@icloud.com": "brndnsvr", # PR #25327 salvage (channel post updates)
"oracle@jarviss-mbp.home": "houenyang-momo", # PR #24014 salvage (quiet noisy errors)
"57119977+OCWC22@users.noreply.github.com": "OCWC22", # PR #24581 salvage (multi-bot exclusive mentions)
"ai-hana-ai@users.noreply.github.com": "ai-hana-ai", # PR #23928 salvage (ignore_root_dm)
"mx.indigo.karasu@gmail.com": "indigokarasu", # PR #26636 salvage (pin user message)
"516972+alber70g@users.noreply.github.com": "alber70g", # PR #25280 salvage (skip-STT + 2GB cap)
"282919977+eliteworkstation94-ai@users.noreply.github.com": "eliteworkstation94-ai", # PR #28157 salvage (group reply session splits)
"androidhtml@yandex.com": "hllqkb",
"25840394+Bongulielmi@users.noreply.github.com": "Bongulielmi",
"jonathan.troyer@overmatch.com": "JTroyerOvermatch",
2026-05-05 08:42:22 -07:00
"harryykyle1@gmail.com": "hharry11",
2026-05-05 08:43:32 -07:00
"wysie@users.noreply.github.com": "wysie",
"ronhi@buildabear1.localdomain": "RonHillDev", # PR #29523 salvage (machine-local commit email)
"moikapy@devmoi.com": "Moikapy", # PR #31527 salvage
fix(xai-sanitize): deepcopy tools_for_api before in-place mutation (#27907) The xAI tool-schema sanitizers (strip_slash_enum, strip_pattern_and_format) mutate their input in place — that's their documented contract. The two call sites (chat_completion_helpers.build_api_kwargs and the auxiliary client) were passing agent.tools straight through, so the first xAI request would permanently strip slash-containing enum constraints and pattern/format keywords from the per-agent tool registry. Effect: any subsequent non-xAI call from the same agent (auxiliary task routed to Anthropic, OpenRouter fallback, mid-session model switch) saw the already-stripped schema with no way for the user to notice from their config. Fix: deepcopy tools_for_api before sanitizing at both call sites. The slash-enum bug itself (xAI 400ing on enums with '/') was fixed earlier by #32443 (Nami4D) — that PR landed the strip but used the sanitizers directly without copying. This salvages #27907's correctness contribution (the deepcopy) while skipping its redundant parallel sanitizer (strip_xai_incompatible_enum_values is functionally equivalent to the existing strip_slash_enum) and its preflight- neutrality argument (we chose model-gated preflight in #32443). 3 new tests in tests/run_agent/test_run_agent_codex_responses.py: - strips_slash_enum_from_outgoing_request — outgoing kwargs has no slash-containing enum values (functional contract preserved). - does_not_mutate_agent_tools — headline #27907 regression. Snapshot agent.tools before build_api_kwargs, assert it survives intact after. Pre-fix this assertion would have caught the mutation. - is_idempotent_across_repeated_calls — three xAI requests in a row each strip cleanly AND don't progressively erode the source schema. 344/344 across tests/agent/test_auxiliary_client.py, tests/agent/transports/test_codex_transport.py, tests/run_agent/test_run_agent_codex_responses.py, and tests/tools/test_schema_sanitizer.py. Co-authored-by: Gabor Barany <barany.gabor@gmail.com>
2026-05-28 23:23:22 -07:00
"barany.gabor@gmail.com": "gbarany", # PR #27907 salvage (xAI sanitizer deepcopy)
"hello@nami4d.tech": "Nami4D", # PR #28490 salvage
2026-05-05 08:46:24 -07:00
"jkausel@gmail.com": "jkausel-ai",
2026-05-05 08:48:04 -07:00
"e.silacandmr@gmail.com": "Es1la",
"51599529+stephen0110@users.noreply.github.com": "stephen0110",
"265632032+sonic-netizen@users.noreply.github.com": "sonic-netizen",
"82531659+mwnickerson@users.noreply.github.com": "mwnickerson",
2026-05-07 05:26:11 -07:00
"sandrohub013@gmail.com": "SandroHub013",
"maciekczech@users.noreply.github.com": "maciekczech",
2026-05-05 08:48:57 -07:00
"154585401+LeonSGP43@users.noreply.github.com": "LeonSGP43",
"cine.dreamer.one@gmail.com": "LeonSGP43",
"david@nutricraft.ca": "cyb0rgk1tty",
"chris+dora@cmullins.io": "cmullins70",
2026-05-05 08:56:40 -07:00
"zjtan1@gmail.com": "zeejaytan",
2026-05-05 08:57:39 -07:00
"asslaenn5@gmail.com": "Aslaaen",
"trae.anderson17@icloud.com": "Tkander1715",
"beardthelion@users.noreply.github.com": "beardthelion",
"orkunozturk@gmail.com": "orcool",
2026-05-05 05:29:49 -07:00
"tangyuanjc@JCdeAIfenshendeMac-mini.local": "tangyuanjc",
"leon@agentlinker.ai": "agentlinker",
"santoshhumagain1887@gmail.com": "npmisantosh",
"39641663+luarss@users.noreply.github.com": "luarss",
"16263913+zccyman@users.noreply.github.com": "zccyman",
"zccyman@users.noreply.github.com": "zccyman", # PR #26998 (auxiliary fallback chain)
"ahmetosrak@Ahmet-MacBook-Air.local": "Osraka",
"98612432+Osraka@users.noreply.github.com": "Osraka",
"112634774+ryptotalent@users.noreply.github.com": "ryptotalent",
"270097726+hookinglau@users.noreply.github.com": "hookinglau",
"5029547+AllynSheep@users.noreply.github.com": "AllynSheep",
"allyn0306@gmail.com": "AllynSheep",
"46887634+aqilaziz@users.noreply.github.com": "aqilaziz",
"gonzes7@gmail.com": "aqilaziz",
"6966326+laoli-no1@users.noreply.github.com": "laoli-no1",
"laoli_no1@163.com": "laoli-no1",
"39730900+NorethSea@users.noreply.github.com": "NorethSea",
"963979204@qq.com": "NorethSea",
"2283389+JamesX88@users.noreply.github.com": "JamesX88",
"JamesX88@users.noreply.github.com": "JamesX88",
2026-05-05 05:35:18 -07:00
"novax635@gmail.com": "novax635",
2026-05-05 05:37:42 -07:00
"krionex1@gmail.com": "Krionex",
2026-05-05 05:39:13 -07:00
"rxdxxxx@users.noreply.github.com": "rxdxxxx",
"ma.haohao2@xydigit.com": "MaHaoHao-ch",
"29756950+revaraver@users.noreply.github.com": "revaraver",
2026-05-05 04:14:40 -07:00
"nexus@eptic.me": "TheEpTic",
2026-05-05 04:16:12 -07:00
"74554762+wmagev@users.noreply.github.com": "wmagev",
2026-05-05 04:18:28 -07:00
"ashermorse@icloud.com": "ashermorse",
2026-05-05 04:40:12 -07:00
"happy5318@users.noreply.github.com": "happy5318",
2026-05-07 06:24:35 -07:00
"anatoliygranichenko@gmail.com": "wabrent",
"cash.williams@acquia.com": "CashWilliams",
2026-05-05 04:22:59 -07:00
"chengoak@users.noreply.github.com": "chengoak",
2026-05-05 04:24:10 -07:00
"mrhanoi@outlook.com": "qxxaa",
"guillaume.meyer@outlook.com": "guillaumemeyer",
fix(telegram): harden DM topic binding — persist through switch_session, rebind on /new Follow-up on @EmelyanenkoK's feat: add Telegram DM topic-mode sessions. Three issues: 1. Split-brain session state. After get_or_create_session() returned a SessionEntry for a topic lane, the handler was mutating .session_id in place to the binding's target, but never persisting the switch through SessionStore. The sessions.json session_key → session_id map kept pointing at the lane's natural id; any reader that reloaded from disk saw the wrong id. Fixed by routing through SessionStore.switch_session(), which _save()s the mapping and ends the old session in SQLite like /resume does. 2. /new inside a topic was a one-message no-op. Reset created a new session but left the telegram_dm_topic_bindings row pointing at the old session_id, so the next message's binding lookup switched right back. Now _handle_reset_command rebinds the topic to the new session_id after reset. 3. is_telegram_session_linked_to_topic and list_unlinked_telegram_sessions_for_user both called apply_telegram_topic_migration() on read, contradicting the PR's own invariant that migration only runs on explicit /topic opt-in. They now tolerate missing topic tables and return empty/False. Also: _telegram_topic_mode_enabled() now only treats True as enabled (not any truthy return), so test fixtures with MagicMock session_db don't accidentally flip every DM into lobby mode — this was breaking 4 pre-existing test_status_command tests. Tests: - New regression: /new inside a topic must update the binding row (test_new_inside_telegram_topic_rewrites_binding_to_new_session). - _make_runner now stubs switch_session so existing restore tests still exercise the new code path. Validated end-to-end with real SessionDB + SessionStore: readers on fresh DB don't create topic tables; enable creates them; binding override persists across SessionStore restart; /new rebinds and the new id survives a restart. Co-authored-by: EmelyanenkoK <emelyanenko.kirill@gmail.com>
2026-05-03 05:34:07 -07:00
"emelyanenko.kirill@gmail.com": "EmelyanenkoK",
"lazycat.manatee@gmail.com": "manateelazycat",
2026-05-05 13:26:03 -07:00
"bzarnitz13@gmail.com": "Beandon13",
2026-05-05 13:31:32 -07:00
"tony@tonysimons.dev": "asimons81",
2026-05-05 13:42:53 -07:00
"jetha@google.com": "jethac",
2026-05-05 13:43:58 -07:00
"jani@0xhoneyjar.xyz": "deep-name",
feat(gateway): add LINE Messaging API platform plugin (#23197) * feat(gateway): add LINE Messaging API platform plugin Adds LINE as a bundled platform plugin under `plugins/platforms/line/`, synthesized from the strongest pieces of seven open community PRs. The adapter requires zero core edits — `Platform("line")` is auto-discovered via the bundled-plugin scan in `gateway/config.py`, and all hooks (setup, env-enablement, cron delivery, standalone send) are wired through `register_platform()` kwargs the way IRC and Teams do it. Highlights merged into one plugin: - **Reply token preferred, Push fallback.** Try the free reply token first (single-use, ~60s TTL); fall back to metered Push when the token is absent, expired, or rejected. (PR #21023) - **Slow-LLM Template Buttons postback.** When the LLM is still running past `LINE_SLOW_RESPONSE_THRESHOLD` (default 45s), the adapter burns the original reply token to send a "Get answer" button bubble. The user taps it to fetch the cached answer via a fresh reply token — also free. State machine: PENDING → READY → DELIVERED, ERROR for cancelled runs (orphan resolves to `LINE_INTERRUPTED_TEXT` after /stop). Set threshold to 0 to disable. (PR #18153) - **Three-allowlist gating** — separate user / group / room allowlists with `LINE_ALLOW_ALL_USERS=true` dev-only escape hatch. (PR #18153) - **Markdown URL preservation.** Strip bold/italic/code-fence/heading markers (LINE renders them literally) but keep `[label](url)` → `label (url)` so URLs stay tappable. (PR #18153) - **System-message bypass** for `⚡ Interrupting`, `⏳ Queued`, etc. — busy-acks reach the user as visible bubbles instead of being swallowed into the postback cache. (PR #18153) - **Media via public HTTPS URLs.** LINE doesn't accept binary uploads; images/audio/video must be HTTPS-reachable. The adapter serves registered tempfiles under `/line/media/<token>/<filename>` from the same aiohttp app. Allowed-roots traversal guard covers `tempfile.gettempdir()`, `/tmp` (→ `/private/tmp` on macOS), and `HERMES_HOME`. `LINE_PUBLIC_URL` overrides URL construction for setups behind tunnels/proxies. (PR #8398) - **5-message-per-call batching.** LINE rejects >5 messages per Reply/Push; smart-chunker caps text at 4500 chars per bubble. - **Inbound dedup** via `webhookEventId` LRU. (PR #21023) - **Self-message filter** via `/v2/bot/info` userId lookup. (PR #21023) - **Loading-animation indicator** wired to LINE's `chat/loading/start` endpoint, DM-only (LINE rejects it for groups/rooms). (PR #21023) - **Out-of-process cron delivery** via `_standalone_send`, so `deliver: line` cron jobs work even when cron runs detached from the gateway. - **Webhook hardening** — 1 MiB body cap, constant-time HMAC-SHA256 signature verification, dedup, scoped lock so two profiles can't bind the same channel. Validation ---------- - `scripts/run_tests.sh tests/gateway/test_line_plugin.py` → 73 passed in 1.05s - `scripts/run_tests.sh tests/gateway/test_line_plugin.py tests/gateway/test_irc_adapter.py tests/gateway/test_plugin_platform_interface.py tests/gateway/test_platform_registry.py tests/gateway/test_config.py` → 193 passed, 7 skipped - E2E import + register + signature roundtrip + `Platform("line")` bundled-plugin discovery verified against current `origin/main`. Closes the seven open LINE PRs (#18153, #16832, #6676, #21023, #14942, #14988, #8398) by superseding them with a single plugin-form implementation that takes the best idea from each. Co-authored-by: pwlee <32443648+leepoweii@users.noreply.github.com> Co-authored-by: Jetha Chan <jetha@google.com> Co-authored-by: Cattia <openclaw@liyangchen.me> Co-authored-by: perng <charles@perng.com> Co-authored-by: Soichiro Yoshimura <soichiro0111.dev@gmail.com> Co-authored-by: David Zhou <77736378+David-0x221Eight@users.noreply.github.com> Co-authored-by: Yu-ga <74749461+yuga-hashimoto@users.noreply.github.com> * docs(platforms): document platform-specific slow-LLM UX pattern Add a 'Platform-Specific Slow-LLM UX' section to the platform-adapter developer guide covering the _keep_typing override pattern that LINE uses for its Template Buttons postback flow. Three subsections: - Pattern: subclass _keep_typing to layer mid-flight UX (with code) - Pattern: subclass send to route through a cache instead of sending - When this pattern is appropriate (vs. always-Push fallback) Plus a short pointer in gateway/platforms/ADDING_A_PLATFORM.md so tree-readers find the prose walkthrough on the docsite. Filed because the LINE plugin (PR #23197) was the first bundled adapter to need this pattern — every prior plugin (irc, teams, google_chat) handles slow responses with the default typing-loop and a regular send_text. Documenting now while the rationale is fresh. --------- Co-authored-by: pwlee <32443648+leepoweii@users.noreply.github.com> Co-authored-by: Jetha Chan <jetha@google.com> Co-authored-by: Cattia <openclaw@liyangchen.me> Co-authored-by: perng <charles@perng.com> Co-authored-by: Soichiro Yoshimura <soichiro0111.dev@gmail.com> Co-authored-by: David Zhou <77736378+David-0x221Eight@users.noreply.github.com> Co-authored-by: Yu-ga <74749461+yuga-hashimoto@users.noreply.github.com>
2026-05-10 06:40:46 -07:00
# LINE messaging plugin (synthesis PR)
"32443648+leepoweii@users.noreply.github.com": "leepoweii",
"openclaw@liyangchen.me": "liyoungc",
"charles@perng.com": "perng",
"soichiro0111.dev@gmail.com": "soichiyo",
"0xde@pieverse.io": "David-0x221Eight",
"77736378+David-0x221Eight@users.noreply.github.com": "David-0x221Eight",
"74749461+yuga-hashimoto@users.noreply.github.com": "yuga-hashimoto",
2026-05-05 13:48:17 -07:00
"xiangyong@zspace.cn": "CES4751",
"harish.kukreja@gmail.com": "counterposition",
"nidhi2894@gmail.com": "nidhi-singh02",
2026-05-05 13:52:36 -07:00
"35294173+Fearvox@users.noreply.github.com": "Fearvox",
"fearvox1015@gmail.com": "Fearvox",
"hypnus.yuan@gmail.com": "Hypnus-Yuan",
2026-05-05 13:55:10 -07:00
"15558128926@qq.com": "xsfX20",
2026-05-05 14:11:05 -07:00
"binhnt.ht.92@gmail.com": "binhnt92",
2026-05-05 14:11:58 -07:00
"johnny@Jons-MBA-M4.local": "acesjohnny",
2026-05-05 14:12:38 -07:00
"1581133593@qq.com": "liu-collab",
"haidaoe@proton.me": "haidao1919",
2026-05-05 14:14:55 -07:00
"50561768+zhanggttry@users.noreply.github.com": "zhanggttry",
"formulahendry@gmail.com": "formulahendry",
2026-05-05 15:12:37 -07:00
"93757150+bogerman1@users.noreply.github.com": "bogerman1",
"132852777+rob-maron@users.noreply.github.com": "rob-maron",
# Matrix parity salvage batch (April 2026)
"sr@samirusani": "samrusani",
"angelclaw@AngelMacBook.local": "angel12",
"charles@cryptoassetrecovery.com": "charles-brooks",
# DeepSeek v4 + Kimi thinking-mode reasoning_content salvage (April 2026)
"luwinyang@deepseek.com": "lsdsjy",
"season.saw@gmail.com": "season179",
"heathley@Heathley-MacBook-Air.local": "heathley",
"maliyldzhn@gmail.com": "heathley",
"vlad19@gmail.com": "dandaka",
"adamrummer@gmail.com": "cyclingwithelephants",
# Temporary tool-progress cleanup salvage (May 2026)
"Mrcharlesiv@gmail.com": "mrcharlesiv",
"nbot@liizfq.top": "liizfq",
"274096618+hermes-agent-dhabibi@users.noreply.github.com": "dhabibi",
"dejie.guo@gmail.com": "JayGwod",
"133716830+0xKingBack@users.noreply.github.com": "0xKingBack",
"daixin1204@gmail.com": "SimbaKingjoe",
feat(gateway): native send_multiple_images for Telegram, Discord, Slack, Mattermost, Email Ports PR #17888's send_multiple_images ABC to every gateway platform that has a native multi-attachment API, so images arrive as a single bundled message instead of N separate ones. Native overrides: - Telegram: send_media_group (10 photos per album, chunks over); animated GIFs peeled off and routed through send_animation (albums don't support animations) - Discord: channel.send(files=[...]) (10 attachments per message, chunks over); URL images downloaded into BytesIO so they render inline; forum channels use create_thread with files=[...] - Slack: files_upload_v2(file_uploads=[...]) (10 per call, chunks over); respects thread_ts; records thread participation - Mattermost: single post with file_ids list (5 per post — Mattermost cap, chunks over) - Email: single SMTP message with multiple MIME attachments (no chunk cap, SMTP size governs); remote URLs remain linked in body (parity with existing send_image) All platforms fall back to the base per-image loop on any failure, so a single bad image in a batch never loses the rest. Matrix, WhatsApp, and single-attachment platforms (BlueBubbles, Feishu, WeCom, WeChat, DingTalk) continue to use the base default loop — their server APIs only accept one attachment per message anyway. Tests: adds tests/gateway/test_send_multiple_images.py with 19 targeted tests covering base default loop, chunking, animation peel-off, fallback paths, and empty-batch no-ops across all five new overrides. Co-authored-by: Maxence Groine <maxence@groine.fr>
2026-04-30 03:39:06 -07:00
"maxence@groine.fr": "MaxyMoos",
"61830395+leprincep35700@users.noreply.github.com": "leprincep35700",
# OpenViking viking_read salvage (April 2026)
"hitesh@gmail.com": "htsh",
"pty819@outlook.com": "pty819",
"pty819@users.noreply.github.com": "pty819",
"14341805+pty819@users.noreply.github.com": "pty819",
"517024110@qq.com": "chennest",
# Curator fixes (Apr 30 2026)
"yuxiangl490@gmail.com": "y0shua1ee",
"manmit0x@gmail.com": "0xDevNinja",
"stevekelly622@gmail.com": "steezkelly",
"brian@dralth.com": "btorresgil",
"momowind@gmail.com": "momowind",
"clockwork-codex@users.noreply.github.com": "misery-hl",
"207811921+misery-hl@users.noreply.github.com": "misery-hl",
"20nik.nosov21@gmail.com": "nik1t7n",
"90299797+nik1t7n@users.noreply.github.com": "nik1t7n",
"suncokret@protonmail.com": "suncokret12",
"mio.imoto.ai@gmail.com": "mioimotoai-lgtm",
test(gateway): isolate plugin adapter imports and guard the anti-pattern Fixes the xdist collision that broke CI on PR #17764, and structurally prevents future plugin-adapter tests from reintroducing it. Problem ------- tests/gateway/test_teams.py (new in this PR) and tests/gateway/test_irc_adapter.py (already on main) both followed the same anti-pattern: sys.path.insert(0, str(_REPO_ROOT / 'plugins' / 'platforms' / '<name>')) from adapter import <Adapter> Every platform plugin ships its own adapter.py, so the bare 'from adapter import ...' races for sys.modules['adapter']. Whichever test collected first in a given xdist worker won; the other crashed at collection with ImportError, and the polluted sys.path cascaded into 19 unrelated test failures across tools/, hermes_cli/, and run_agent/ in the same worker. Fix --- 1. tests/gateway/_plugin_adapter_loader.py (new): shared helper load_plugin_adapter('<name>') that imports plugins/platforms/<name>/adapter.py via importlib.util under the unique module name plugin_adapter_<name>. Zero sys.path mutation, no possibility of collision. 2. tests/gateway/test_irc_adapter.py and tests/gateway/test_teams.py: migrated to the helper. All 'from adapter import ...' statements (including the ones inside test methods) are replaced with module-level attribute access on the loaded module. 3. tests/gateway/conftest.py: new pytest_configure guard that AST-scans every test_*.py under tests/gateway/ at session start and fails the run with a pointer to the helper if any test uses sys.path.insert into plugins/platforms/ OR a bare 'import adapter' / 'from adapter import'. Runs on the xdist controller only (skipped in workers). The next plugin adapter test that tries to reintroduce this pattern gets rejected at collection time with a clear remediation message. 4. scripts/release.py: add aamirjawaid@microsoft.com -> heyitsaamir to AUTHOR_MAP so the check-attribution workflow passes. Validation ---------- scripts/run_tests.sh tests/gateway/ 4194 passed scripts/run_tests.sh tests/gateway/test_{teams,irc}* 72 passed (both orderings) scripts/run_tests.sh <11 prev-failing test files> 398 passed Guard triggers correctly on both Path-operator and string-literal forms of the anti-pattern.
2026-04-30 01:03:49 -07:00
"aamirjawaid@microsoft.com": "heyitsaamir",
"johnnncenaaa77@gmail.com": "johnncenae",
"thomasjhon6666@gmail.com": "ThomassJonax",
"focusflow.app.help@gmail.com": "yes999zc",
"rob@atlas.lan": "rmoen",
# Slack ephemeral slash-ack salvage (May 2026)
"probepark@users.noreply.github.com": "probepark",
# Slack batch salvage (May 2026)
"280484231+prive-fe-bot@users.noreply.github.com": "priveperfumes",
"amr@ghanem.sa": "amroessam",
"paperlantern.agent@gmail.com": "Hinotoi-agent",
"valda@underscore.jp": "valda",
fix(model_tools): cancel coroutine on timeout so worker thread exits + log full traceback _run_async() bridges sync tool handlers to async code. When the handler is invoked from inside a running event loop (gateway / nested async), it spawns a worker thread and blocks on future.result(timeout=300). Before this change, a coroutine that ran past 300s leaked its worker thread: - future.cancel() is a no-op on a running ThreadPoolExecutor future (cancel only works on not-yet-started work). - pool.shutdown(wait=False, cancel_futures=True) let the caller proceed but the worker kept running the coroutine until it returned on its own. Every tool timeout leaked one thread. In long-lived gateway / RL sessions this is cumulative. The fix replaces bare asyncio.run() with a worker wrapper that creates its own event loop. On timeout, _run_async schedules task.cancel() on that loop via call_soon_threadsafe, then shuts the pool down with wait=False so the caller returns immediately. The coroutine observes CancelledError at its next await and the worker thread exits cleanly. Also switches logger.error() to logger.exception() in the top-level handle_function_call() except block so tool failures produce full stack traces in errors.log instead of just the message. Related: #17420 (contributor flagged the leak; the original fix used pool.shutdown(wait=True) which would have converted the leak into a hang — caller blocks forever on the same stuck coroutine). Credit for identifying the leak goes to the contributor. Co-authored-by: 0z! <162235745+0z1-ghb@users.noreply.github.com>
2026-04-29 04:56:33 -07:00
"162235745+0z1-ghb@users.noreply.github.com": "0z1-ghb",
"yes999zc@163.com": "yes999zc",
"343873859@qq.com": "DrStrangerUJN",
2026-04-29 14:17:42 -07:00
"252818347@qq.com": "hejuntt1014",
"uzmpsk.dilekakbas@gmail.com": "dlkakbs",
fix(gateway): use transcript timestamp for auto-continue freshness Follow-up to PR #16802 (BeliefanX). The original fix read `agent_history[-1].get("timestamp")` for the tool-tail freshness gate, but `gateway/run.py` strips the `timestamp` field off all tool/tool_call rows when building `agent_history` from the raw transcript (see `clean_msg = {k: v for k, v in msg.items() if k != "timestamp"}`). At runtime the tool-tail branch always saw `None` and silently took the legacy-fresh path — the stale-guard never fired for the tool-tail case it was supposed to cover. Changes: - Read the freshness signal from the RAW `history` list (via new `_last_transcript_timestamp()` helper) BEFORE the strip. Both the resume_pending branch and the tool-tail branch use this single signal, replacing the two divergent ones. - Default window bumped 15 min → 1 hour via new `_AUTO_CONTINUE_FRESHNESS_SECS_DEFAULT`. The 15-minute default was shorter than the default `gateway_timeout` of 30 min, so a legitimate long-running turn interrupted near its timeout boundary and resumed shortly after would have been misclassified as stale. - Configurable via `config.yaml` `agent.gateway_auto_continue_freshness` (bridged to `HERMES_AUTO_CONTINUE_FRESHNESS` at gateway startup — same pattern as `gateway_timeout`). Set to 0 to disable the gate. - `_coerce_gateway_timestamp` now explicitly rejects bool (which is a subclass of int and would otherwise coerce to 0.0/1.0). - Tests rewritten to exercise the real production data shape: raw `history` → `_build_agent_history` strip → freshness decision. A regression guard (`test_stale_tool_tail_with_production_data_shape`) asserts `agent_history` tool rows carry NO timestamp, protecting against someone "fixing" the original bug by re-adding the stripped field (which would break the OpenAI tool-result message contract). Add BeliefanX to scripts/release.py AUTHOR_MAP. E2E verified: config.yaml → env var bridge → helper returns configured value; default 1h window; malformed/empty env var falls back to default; ISO-Z timestamps parse; ms-epoch coerced; bool rejected.
2026-04-28 05:00:26 -07:00
"beliefanx@gmail.com": "BeliefanX",
"changchun989@proton.me": "changchun989",
"jefferson@heimdallstrategy.com": "Mind-Dragon",
"44753291+Nanako0129@users.noreply.github.com": "Nanako0129",
"steve.westerhouse@origami-analytics.com": "westers",
2026-04-29 08:17:50 -07:00
"yeyitech@users.noreply.github.com": "yeyitech",
"260878550+beenherebefore@users.noreply.github.com": "beenherebefore",
2026-04-29 10:29:59 -07:00
"79389617+txbxxx@users.noreply.github.com": "txbxxx",
"liuhao03@bilibili.com": "liuhao1024",
"130918800+devorun@users.noreply.github.com": "devorun",
"surat.s@itm.kmutnb.ac.th": "beesrsj2500",
"beesr@bee.localdomain": "beesrsj2500",
"mind-dragon@nous.research": "Mind-Dragon",
"juntingpublic@gmail.com": "JustinUssuri",
"mtf201013@gmail.com": "ma-pony",
"sonoyuncudmr@gmail.com": "Sonoyunchu",
"43525405+yatesjalex@users.noreply.github.com": "yatesjalex",
"maks.mir@yahoo.com": "say8hi",
2026-04-28 18:51:03 +05:30
"27719690+Mirac1eSky@users.noreply.github.com": "Mirac1eSky",
"web3blind@users.noreply.github.com": "web3blind",
"julia@alexland.us": "alexg0bot",
"christian@scheid.tech": "scheidti",
# Moonshot schema anyOf+enum salvage (May 2026)
"git@local.invalid": "hendrixfreire",
"1060770+benjaminsehl@users.noreply.github.com": "benjaminsehl",
"nerijusn76@gmail.com": "Nerijusas",
# Compaction salvage batch (May 2026)
"MacroAnarchy@users.noreply.github.com": "MacroAnarchy",
"itonov@proton.me": "Ito-69",
"glesstech@gmail.com": "georgeglessner",
"maxim.smetanin@gmail.com": "maxims-oss",
# Codex Spark restoration salvage (May 2026)
"olegwn@gmail.com": "nederev",
"vesper@askclaw.dev": "askclaw-vesper",
"nazirulhafiy@gmail.com": "nazirulhafiy",
"CREWorx@users.noreply.github.com": "BadTechBandit",
"yoimexex@gmail.com": "Yoimex",
"6548898+romanornr@users.noreply.github.com": "romanornr",
2026-04-27 04:57:39 -07:00
"foxion37@gmail.com": "foxion37",
"bloodcarter@gmail.com": "bloodcarter",
"scott@scotttrinh.com": "scotttrinh",
"quocanh261997@gmail.com": "quocanh261997",
"savanne.kham@protonmail.com": "savanne-kham", # PR #28958 salvage (strip tool_name for strict providers)
# contributors (from noreply pattern)
"david.vv@icloud.com": "davidvv",
"wangqiang@wangqiangdeMac-mini.local": "xiaoqiang243",
2026-04-17 18:56:06 -07:00
"snreynolds2506@gmail.com": "snreynolds",
"35742124+0xbyt4@users.noreply.github.com": "0xbyt4",
"71184274+MassiveMassimo@users.noreply.github.com": "MassiveMassimo",
"massivemassimo@users.noreply.github.com": "MassiveMassimo",
"82637225+kshitijk4poor@users.noreply.github.com": "kshitijk4poor",
"keifergu@tencent.com": "keifergu",
"kshitijk4poor@users.noreply.github.com": "kshitijk4poor",
fix(skills/comfyui): correct hallucinated node names and registry slugs Self-review caught several errors in the previous commit: Frontmatter - Replace non-standard `requires_runtime` / `requires_tooling` fields with the documented `compatibility:` field (parsed by tools/skills_tool.py). - Drop the `audit-v5` author tag I added unnecessarily. MODEL_LOADERS catalog - Remove `IPAdapterUnifiedLoader` (input `preset` is an enum, not a file). - Remove `IPAdapterInsightFaceLoader` and `InsightFaceLoader` (input `provider` is a GPU backend selector, not a model file). These would have flagged enum values like "STANDARD" or "CUDA" as missing model files. - Add "NB:" comment explaining `BasicGuider` has no `cfg` input (the original PARAM_PATTERNS entry would never have matched). - Remove `SamplerCustomAdvanced.noise_seed` from PARAM_PATTERNS — that node takes a NOISE input from RandomNoise, not a seed field directly. NODE_TO_PACKAGE registry slugs - Verified all 18 packages against api.comfy.org and fixed: - `comfyui-essentials` → `comfyui_essentials` (underscore, not hyphen) - `comfyui-gguf` → `ComfyUI-GGUF` (case-sensitive) - `comfyui-photomaker-plus` → `ComfyUI-PhotoMaker-Plus` - `comfyui-wanvideowrapper` → `ComfyUI-WanVideoWrapper` - ComfyUI-HunyuanVideoWrapper isn't on the registry; surface a git-URL install hint via new NODE_TO_GIT_URL fallback so the user can install via ComfyUI-Manager's /manager/queue/install endpoint. Wrong class names - `Canny` → `CannyEdgePreprocessor` (controlnet-aux registers the latter, the former never appears in /object_info). - Add `Zoe_DepthAnythingPreprocessor` and `AnimalPosePreprocessor` while fixing controlnet-aux. - Remove `Reroute (rgthree)` (rgthree's Reroute is JS-only — no Python class, never appears in /object_info). - Add `Display Int (rgthree)` (sibling of Display Any). - Move `UltralyticsDetectorProvider` from `comfyui-impact-pack` to `comfyui-impact-subpack` (separate package, registered there). Tests - Update test_packages_are_safe_for_shell to accept case-mixed slugs (the registry uses both ComfyUI- and comfyui_ prefixes inconsistently). Replaced the lowercase-only assertion with a shell-safe regex check. - 117 tests still pass (105 unit + 8 cloud + 4 cross-host). Attribution - Add `SHL0MS@users.noreply.github.com` mapping to scripts/release.py AUTHOR_MAP so check-attribution CI passes.
2026-04-29 21:38:50 -04:00
"SHL0MS@users.noreply.github.com": "SHL0MS",
"abner.the.foreman@agentmail.to": "Abnertheforeman",
feat(minimax-oauth): full integration with peer OAuth providers Close integration gaps discovered by auditing qwen-oauth's file coverage. These are surfaces the original salvage missed — they all existed on main and were added in the 747 commits since PR #15203 was opened. Coverage added: - agent/credential_pool.py: seed pool from auth.json providers.minimax-oauth so `hermes auth list` reflects logged-in state and `hermes auth remove minimax-oauth <N>` works through the standard flow. - agent/credential_sources.py: register RemovalStep for minimax-oauth with suppression-aware `_clear_auth_store_provider`. - agent/models_dev.py: PROVIDER_TO_MODELS_DEV mapping (-> 'minimax' family). - hermes_cli/providers.py: HermesOverlay entry (anthropic_messages transport, oauth_external auth_type, api.minimax.io/anthropic base). - hermes_cli/model_normalize.py: add to _MATCHING_PREFIX_STRIP_PROVIDERS so `minimax-oauth/MiniMax-M2.7` in config.yaml gets correctly repaired. - hermes_cli/status.py: render MiniMax OAuth block in `hermes doctor` (logged-in / region / expires_at / error). - hermes_cli/web_server.py: register in OAUTH_PROVIDER_REGISTRY + dispatch branch in _resolve_provider_status so the dashboard auth page shows it. - website/docs/integrations/providers.md: full 'MiniMax (OAuth)' section. - website/docs/reference/cli-commands.md: --provider enum. - website/docs/user-guide/features/fallback-providers.md: fallback table row. - scripts/release.py AUTHOR_MAP: amanning3390 mapping (CI gate).
2026-04-29 08:25:27 -07:00
"adam.manning@pro-serveinc.com": "amanning3390",
"thomasgeorgevii09@gmail.com": "tochukwuada",
"sb@wmc.sh": "zicochaos",
"harryykyle1@gmail.com": "hharry11",
"kshitijk4poor@gmail.com": "kshitijk4poor",
"1294707+Tosko4@users.noreply.github.com": "Tosko4",
"keira.voss94@gmail.com": "keiravoss94",
"16443023+stablegenius49@users.noreply.github.com": "stablegenius49",
"fqsy1416@gmail.com": "EKKOLearnAI",
"octo-patch@github.com": "octo-patch",
"math0r-be@github.com": "math0r-be",
"simbamax99@gmail.com": "simbam99",
"iris@growthpillars.co": "irispillars",
"185121704+stablegenius49@users.noreply.github.com": "stablegenius49",
"101283333+batuhankocyigit@users.noreply.github.com": "batuhankocyigit",
"255305877+ismell0992-afk@users.noreply.github.com": "ismell0992-afk",
"cyprian@ironin.pl": "iRonin",
"valdi.jorge@gmail.com": "jvcl",
"q19dcp@gmail.com": "aj-nt",
"ebukau84@gmail.com": "UgwujaGeorge",
"francip@gmail.com": "francip",
"omni@comelse.com": "omnissiah-comelse",
"oussama.redcode@gmail.com": "mavrickdeveloper",
"126368201+vilkasdev@users.noreply.github.com": "vilkasdev",
"137614867+cutepawss@users.noreply.github.com": "cutepawss",
"96793918+memosr@users.noreply.github.com": "memosr",
"mehmet.sr35@gmail.com": "memosr",
fix(feishu): queue inbound events when adapter loop not ready (#5499) (#11372) Inbound Feishu messages arriving during brief windows when the adapter loop is unavailable (startup/restart transitions, network-flap reconnect) were silently dropped with a WARNING log. This matches the symptom in issue #5499 — and users have reported seeing only a subset of their messages reach the agent. Fix: queue pending events in a thread-safe list and spawn a single drainer thread that replays them once the loop becomes ready. Covers these scenarios: * Queue events instead of dropping when loop is None/closed * Single drainer handles the full queue (not thread-per-event) * Thread-safe with threading.Lock on the queue and schedule flag * Handles mid-drain bursts (new events arrive while drainer is working) * Handles RuntimeError if loop closes between check and submit * Depth cap (1000) prevents unbounded growth during extended outages * Drops queue cleanly on disconnect rather than holding forever * Safety timeout (120s) prevents infinite retention on broken adapters Based on the approach proposed in #4789 by milkoor, rewritten for thread-safety and correctness. Test plan: * 5 new unit tests (TestPendingInboundQueue) — all passing * E2E test with real asyncio loop + fake WS thread: 10-event burst before loop ready → all 10 delivered in order * E2E concurrent burst test: 20 events queued, 20 more arrive during drainer dispatch → all 40 delivered, no loss, no duplicates * All 111 existing feishu tests pass Related: #5499, #4789 Co-authored-by: milkoor <milkoor@users.noreply.github.com>
2026-04-16 20:36:59 -07:00
"milkoor@users.noreply.github.com": "milkoor",
"xuerui911@gmail.com": "Fatty911",
"131039422+SHL0MS@users.noreply.github.com": "SHL0MS",
"77628552+raulvidis@users.noreply.github.com": "raulvidis",
"145567217+Aum08Desai@users.noreply.github.com": "Aum08Desai",
"256820943+kshitij-eliza@users.noreply.github.com": "kshitij-eliza",
"jiechengwu@pony.ai": "Jason2031",
"44278268+shitcoinsherpa@users.noreply.github.com": "shitcoinsherpa",
"104278804+Sertug17@users.noreply.github.com": "Sertug17",
"112503481+caentzminger@users.noreply.github.com": "caentzminger",
"258577966+voidborne-d@users.noreply.github.com": "voidborne-d",
"3820588+ddupont808@users.noreply.github.com": "ddupont808",
"liusway405@gmail.com": "voidborne-d",
"xydarcher@uestc.edu.cn": "Readon",
"sir_even@icloud.com": "sirEven",
"36056348+sirEven@users.noreply.github.com": "sirEven",
"70424851+insecurejezza@users.noreply.github.com": "insecurejezza",
"jezzahehn@gmail.com": "JezzaHehn",
"barnacleboy.jezzahehn@agentmail.to": "JezzaHehn",
"254021826+dodo-reach@users.noreply.github.com": "dodo-reach",
"259807879+Bartok9@users.noreply.github.com": "Bartok9",
fix(gateway): unify MEDIA: extraction extension set + close the unknown-ext black hole (#34517) (#34844) MEDIA:<path> tags for .md/.json/.yaml/.xml/.html and other document extensions were silently dropped. extract_media() carried a narrow extension allowlist that omitted them, while extract_local_files() had a broad one. The dispatch sites then ran an unconditional re.sub(r'MEDIA:\\s*\\S+', '') that stripped the tag from the body even when extract_media had not matched it — so extract_local_files (broad list) ran on text where the path was already gone, and the file was delivered by neither path. - Add MEDIA_DELIVERY_EXTS in gateway/platforms/base.py as the single source of truth; extract_media and extract_local_files both derive their extension set from it (no more drift). - Replace the loose MEDIA cleanup at the non-streaming dispatch site (base.py) and the streaming consumer (stream_consumer.py) with the shared, extension-anchored MEDIA_TAG_CLEANUP_RE. A MEDIA: tag with an unknown extension is left in the body so the bare-path detector can still pick it up instead of being black-holed. - Chain cleaned text through extract_media -> extract_images -> extract_local_files in run.py's post-stream media delivery (it was dropping the cleaned text and rescanning raw text with MEDIA: tags). - Regression tests covering both halves: previously-dropped extensions now extract, and unknown-ext paths survive the cleanup. Consolidates the MEDIA extension-allowlist PR cluster. Co-authored-by: Bartok9 <259807879+Bartok9@users.noreply.github.com> Co-authored-by: banditburai <123342691+banditburai@users.noreply.github.com> Co-authored-by: Kyzcreig <9063726+Kyzcreig@users.noreply.github.com>
2026-05-29 13:24:01 -07:00
"123342691+banditburai@users.noreply.github.com": "banditburai",
"9063726+Kyzcreig@users.noreply.github.com": "Kyzcreig",
"270082434+crayfish-ai@users.noreply.github.com": "crayfish-ai",
"241404605+MestreY0d4-Uninter@users.noreply.github.com": "MestreY0d4-Uninter",
2026-04-14 14:19:49 -07:00
"268667990+Roy-oss1@users.noreply.github.com": "Roy-oss1",
"27917469+nosleepcassette@users.noreply.github.com": "nosleepcassette",
"241404605+MestreY0d4-Uninter@users.noreply.github.com": "MestreY0d4-Uninter",
2026-04-16 05:51:37 -07:00
"109555139+davetist@users.noreply.github.com": "davetist",
"39405770+yyq4193@users.noreply.github.com": "yyq4193",
"Asunfly@users.noreply.github.com": "Asunfly",
"2500400+honghua@users.noreply.github.com": "honghua",
2026-04-20 05:10:02 -07:00
"462836+jplew@users.noreply.github.com": "jplew",
"nish3451@users.noreply.github.com": "nish3451",
"Mibayy@users.noreply.github.com": "Mibayy",
"mibayy@users.noreply.github.com": "Mibayy",
"mibay@clawhub.io": "Mibayy",
"louismichalot@hotmail.com": "Mibayy",
2026-04-19 15:34:02 +05:30
"135070653+sgaofen@users.noreply.github.com": "sgaofen",
"lzy.dev@gmail.com": "zhiyanliu",
"me@janstepanovsky.cz": "hhhonzik",
"139848623+hhuang91@users.noreply.github.com": "hhuang91",
"s.ozaki@ebinou.net": "Satoshi-agi",
"10774721+kunlabs@users.noreply.github.com": "kunlabs",
"110560187+Wang-tianhao@users.noreply.github.com": "Wang-tianhao",
"170458616+ghostmfr@users.noreply.github.com": "ghostmfr",
"1848670+mewwts@users.noreply.github.com": "mewwts",
"1930707+haru398801@users.noreply.github.com": "haru398801",
"rapabelias@gmail.com": "badgerbees",
"xnb888@proton.me": "xnbi",
"xiahu889889@proton.me": "xiahu88988",
fix(anthropic): complete third-party Anthropic-compatible provider support (#12846) Third-party gateways that speak the native Anthropic protocol (MiniMax, Zhipu GLM, Alibaba DashScope, Kimi, LiteLLM proxies) now work end-to-end with the same feature set as direct api.anthropic.com callers. Synthesizes eight stale community PRs into one consolidated change. Five fixes: - URL detection: consolidate three inline `endswith("/anthropic")` checks in runtime_provider.py into the shared _detect_api_mode_for_url helper. Third-party /anthropic endpoints now auto-resolve to api_mode=anthropic_messages via one code path instead of three. - OAuth leak-guard: all five sites that assign `_is_anthropic_oauth` (__init__, switch_model, _try_refresh_anthropic_client_credentials, _swap_credential, _try_activate_fallback) now gate on `provider == "anthropic"` so a stale ANTHROPIC_TOKEN never trips Claude-Code identity injection on third-party endpoints. Previously only 2 of 5 sites were guarded. - Prompt caching: new method `_anthropic_prompt_cache_policy()` returns `(should_cache, use_native_layout)` per endpoint. Replaces three inline conditions and the `native_anthropic=(api_mode=='anthropic_messages')` call-site flag. Native Anthropic and third-party Anthropic gateways both get the native cache_control layout; OpenRouter gets envelope layout. Layout is persisted in `_primary_runtime` so fallback restoration preserves the per-endpoint choice. - Auxiliary client: `_try_custom_endpoint` honors `api_mode=anthropic_messages` and builds `AnthropicAuxiliaryClient` instead of silently downgrading to an OpenAI-wire client. Degrades gracefully to OpenAI-wire when the anthropic SDK isn't installed. - Config hygiene: `_update_config_for_provider` (hermes_cli/auth.py) clears stale `api_key`/`api_mode` when switching to a built-in provider, so a previous MiniMax custom endpoint's credentials can't leak into a later OpenRouter session. - Truncation continuation: length-continuation and tool-call-truncation retry now cover `anthropic_messages` in addition to `chat_completions` and `bedrock_converse`. Reuses the existing `_build_assistant_message` path via `normalize_anthropic_response()` so the interim message shape is byte-identical to the non-truncated path. Tests: 6 new files, 42 test cases. Targeted run + tests/run_agent, tests/agent, tests/hermes_cli all pass (4554 passed). Synthesized from (credits preserved via Co-authored-by trailers): #7410 @nocoo — URL detection helper #7393 @keyuyuan — OAuth 5-site guard #7367 @n-WN — OAuth guard (narrower cousin, kept comment) #8636 @sgaofen — caching helper + native-vs-proxy layout split #10954 @Only-Code-A — caching on anthropic_messages+Claude #7648 @zhongyueming1121 — aux client anthropic_messages branch #6096 @hansnow — /model switch clears stale api_mode #9691 @TroyMitchell911 — anthropic_messages truncation continuation Closes: #7366, #8294 (third-party Anthropic identity + caching). Supersedes: #7410, #7367, #7393, #8636, #10954, #7648, #6096, #9691. Rejects: #9621 (OpenAI-wire caching with incomplete blocklist — risky), #7242 (superseded by #9691, stale branch), #8321 (targets smart_model_routing which was removed in #12732). Co-authored-by: nocoo <nocoo@users.noreply.github.com> Co-authored-by: Keyu Yuan <leoyuan0099@gmail.com> Co-authored-by: Zoee <30841158+n-WN@users.noreply.github.com> Co-authored-by: sgaofen <135070653+sgaofen@users.noreply.github.com> Co-authored-by: Only-Code-A <bxzt2006@163.com> Co-authored-by: zhongyueming <mygamez@163.com> Co-authored-by: Xiaohan Li <hansnow@users.noreply.github.com> Co-authored-by: Troy Mitchell <i@troy-y.org>
2026-04-19 22:43:09 -07:00
"nocoo@users.noreply.github.com": "nocoo",
"30841158+n-WN@users.noreply.github.com": "n-WN",
"tsuijinglei@gmail.com": "hiddenpuppy",
"buraysandro9@gmail.com": "ygd58",
"jerome@clawwork.ai": "HiddenPuppy",
"jerome.benoit@sap.com": "jerome-benoit",
"wysie@users.noreply.github.com": "Wysie",
fix(anthropic): complete third-party Anthropic-compatible provider support (#12846) Third-party gateways that speak the native Anthropic protocol (MiniMax, Zhipu GLM, Alibaba DashScope, Kimi, LiteLLM proxies) now work end-to-end with the same feature set as direct api.anthropic.com callers. Synthesizes eight stale community PRs into one consolidated change. Five fixes: - URL detection: consolidate three inline `endswith("/anthropic")` checks in runtime_provider.py into the shared _detect_api_mode_for_url helper. Third-party /anthropic endpoints now auto-resolve to api_mode=anthropic_messages via one code path instead of three. - OAuth leak-guard: all five sites that assign `_is_anthropic_oauth` (__init__, switch_model, _try_refresh_anthropic_client_credentials, _swap_credential, _try_activate_fallback) now gate on `provider == "anthropic"` so a stale ANTHROPIC_TOKEN never trips Claude-Code identity injection on third-party endpoints. Previously only 2 of 5 sites were guarded. - Prompt caching: new method `_anthropic_prompt_cache_policy()` returns `(should_cache, use_native_layout)` per endpoint. Replaces three inline conditions and the `native_anthropic=(api_mode=='anthropic_messages')` call-site flag. Native Anthropic and third-party Anthropic gateways both get the native cache_control layout; OpenRouter gets envelope layout. Layout is persisted in `_primary_runtime` so fallback restoration preserves the per-endpoint choice. - Auxiliary client: `_try_custom_endpoint` honors `api_mode=anthropic_messages` and builds `AnthropicAuxiliaryClient` instead of silently downgrading to an OpenAI-wire client. Degrades gracefully to OpenAI-wire when the anthropic SDK isn't installed. - Config hygiene: `_update_config_for_provider` (hermes_cli/auth.py) clears stale `api_key`/`api_mode` when switching to a built-in provider, so a previous MiniMax custom endpoint's credentials can't leak into a later OpenRouter session. - Truncation continuation: length-continuation and tool-call-truncation retry now cover `anthropic_messages` in addition to `chat_completions` and `bedrock_converse`. Reuses the existing `_build_assistant_message` path via `normalize_anthropic_response()` so the interim message shape is byte-identical to the non-truncated path. Tests: 6 new files, 42 test cases. Targeted run + tests/run_agent, tests/agent, tests/hermes_cli all pass (4554 passed). Synthesized from (credits preserved via Co-authored-by trailers): #7410 @nocoo — URL detection helper #7393 @keyuyuan — OAuth 5-site guard #7367 @n-WN — OAuth guard (narrower cousin, kept comment) #8636 @sgaofen — caching helper + native-vs-proxy layout split #10954 @Only-Code-A — caching on anthropic_messages+Claude #7648 @zhongyueming1121 — aux client anthropic_messages branch #6096 @hansnow — /model switch clears stale api_mode #9691 @TroyMitchell911 — anthropic_messages truncation continuation Closes: #7366, #8294 (third-party Anthropic identity + caching). Supersedes: #7410, #7367, #7393, #8636, #10954, #7648, #6096, #9691. Rejects: #9621 (OpenAI-wire caching with incomplete blocklist — risky), #7242 (superseded by #9691, stale branch), #8321 (targets smart_model_routing which was removed in #12732). Co-authored-by: nocoo <nocoo@users.noreply.github.com> Co-authored-by: Keyu Yuan <leoyuan0099@gmail.com> Co-authored-by: Zoee <30841158+n-WN@users.noreply.github.com> Co-authored-by: sgaofen <135070653+sgaofen@users.noreply.github.com> Co-authored-by: Only-Code-A <bxzt2006@163.com> Co-authored-by: zhongyueming <mygamez@163.com> Co-authored-by: Xiaohan Li <hansnow@users.noreply.github.com> Co-authored-by: Troy Mitchell <i@troy-y.org>
2026-04-19 22:43:09 -07:00
"leoyuan0099@gmail.com": "keyuyuan",
"bxzt2006@163.com": "Only-Code-A",
"i@troy-y.org": "TroyMitchell911",
"mygamez@163.com": "zhongyueming1121",
"hansnow@users.noreply.github.com": "hansnow",
"134848055+UNLINEARITY@users.noreply.github.com": "UNLINEARITY",
"ben.burtenshaw@gmail.com": "burtenshaw",
2026-04-22 21:15:24 +05:30
"roopaknijhara@gmail.com": "rnijhara",
"josephzcan@gmail.com": "j0sephz",
# contributors (manual mapping from git names)
2026-04-15 13:03:31 +00:00
"ahmedsherif95@gmail.com": "asheriif",
"dyxushuai@gmail.com": "dyxushuai",
"33860762+etcircle@users.noreply.github.com": "etcircle",
"liujinkun@bytedance.com": "liujinkun2025",
"dmayhem93@gmail.com": "dmahan93",
"fr@tecompanytea.com": "ifrederico",
"cdanis@gmail.com": "cdanis",
"samherring99@gmail.com": "samherring99",
"desaiaum08@gmail.com": "Aum08Desai",
"shannon.sands.1979@gmail.com": "shannonsands",
"shannon@nousresearch.com": "shannonsands",
"abdi.moya@gmail.com": "AxDSan",
"eri@plasticlabs.ai": "Erosika",
"hjcpuro@gmail.com": "hjc-puro",
"xaydinoktay@gmail.com": "aydnOktay",
"abdullahfarukozden@gmail.com": "Farukest",
"lovre.pesut@gmail.com": "rovle",
"xjtumj@gmail.com": "mengjian-github",
"kevinskysunny@gmail.com": "kevinskysunny",
"xiewenxuan462@gmail.com": "yule975",
"yiweimeng.dlut@hotmail.com": "meng93",
"hakanerten02@hotmail.com": "teyrebaz33",
"linux2010@users.noreply.github.com": "Linux2010",
"elmatadorgh@users.noreply.github.com": "elmatadorgh",
feat(curator): add archive and prune subcommands (#20200) * fix(curator): protect hub skills by frontmatter name * test(skill_usage): add mark_agent_created to regression test The cherry-picked test predates #19618/#19621 which rewrote list_agent_created_skill_names() to require an explicit created_by: 'agent' provenance marker. Without mark_agent_created(), my-skill is excluded from the list and the positive assertion fails. * feat(curator): add archive and prune subcommands Adds 'hermes curator archive <skill>' and 'hermes curator prune [--days N] [--yes] [--dry-run]' alongside the existing status, run, pause, resume, pin, unpin, restore, backup, rollback verbs. These are the two genuinely new user-facing verbs requested in #19384. The other verbs proposed there ('stats' and 'restore') already exist as 'curator status' and 'curator restore', so no duplicate surface is added — all skill lifecycle commands live under the single 'hermes curator' namespace. - archive: manual archive of an agent-created skill. Refuses pinned skills with a hint pointing at 'hermes curator unpin'. - prune: bulk-archive unpinned skills idle for >= N days (default 90). Falls back to created_at when last_activity_at is null so never-used skills can still be pruned. --dry-run previews, --yes skips prompt. Adapted from @elmatadorgh's PR #19454 which placed the same verbs under 'hermes skills' with a separate hermes_cli/skills_config.py handler and rich table for stats. The 'stats' and 'restore' parts of that PR duplicated existing surface, so only archive and prune are kept, rewritten to match hermes_cli/curator.py's existing plain-text handler style. Tests rewritten from scratch against the new handlers. Closes #19384 Co-authored-by: elmatadorgh <coktinbaran5@gmail.com> --------- Co-authored-by: LeonSGP43 <cine.dreamer.one@gmail.com> Co-authored-by: elmatadorgh <coktinbaran5@gmail.com>
2026-05-05 05:15:54 -07:00
"coktinbaran5@gmail.com": "elmatadorgh",
"alexazzjjtt@163.com": "alexzhu0",
"1180176+Swift42@users.noreply.github.com": "Swift42",
"ruzzgarcn@gmail.com": "Ruzzgar",
"yukipukikedy@gmail.com": "Yukipukii1",
"alireza78.crypto@gmail.com": "alireza78a",
"brooklyn.bb.nicholson@gmail.com": "OutThisLife",
"withapurpose37@gmail.com": "StefanIsMe",
2026-04-15 14:59:35 -07:00
"4317663+helix4u@users.noreply.github.com": "helix4u",
"ifkellx@users.noreply.github.com": "Ifkellx",
"331214+counterposition@users.noreply.github.com": "counterposition",
"blspear@gmail.com": "BrennerSpear",
"akhater@gmail.com": "akhater",
"Cos_Admin@PTG-COS.lodluvup4uaudnm3ycd14giyug.xx.internal.cloudapp.net": "akhater",
2026-04-15 17:10:02 -07:00
"239876380+handsdiff@users.noreply.github.com": "handsdiff",
"hesapacicam112@gmail.com": "etherman-os",
"mark.ramsell@rivermounts.com": "mark-ramsell",
"taeng02@icloud.com": "taeng0204",
"gpickett00@gmail.com": "gpickett00",
"mcosma@gmail.com": "wakamex",
"clawdia.nash@proton.me": "clawdia-nash",
"pickett.austin@gmail.com": "austinpickett",
"dangtc94@gmail.com": "dieutx",
"jaisehgal11299@gmail.com": "jaisup",
"percydikec@gmail.com": "PercyDikec",
"noonou7@gmail.com": "HenkDz",
# Azure Foundry salvage (PRs #9029, #4599, #10086, #8766)
"tech@smartlogics.net": "TechPrototyper",
"637186+HangGlidersRule@users.noreply.github.com": "HangGlidersRule",
"pein892@gmail.com": "pein892",
"dean.kerr@gmail.com": "deankerr",
"socrates1024@gmail.com": "socrates1024",
"seanalt555@gmail.com": "Salt-555",
"satelerd@gmail.com": "satelerd",
"dan@danlynn.com": "danklynn",
"mattmaximo@hotmail.com": "MattMaximo",
2026-04-24 14:18:15 -04:00
"MatthewRHardwick@gmail.com": "mrhwick",
"149063006+j3ffffff@users.noreply.github.com": "j3ffffff",
"A-FdL-Prog@users.noreply.github.com": "A-FdL-Prog",
"l0hde@users.noreply.github.com": "l0hde",
"difujia@users.noreply.github.com": "difujia",
"vominh1919@gmail.com": "vominh1919",
"yue.gu2023@gmail.com": "YueLich",
"51783311+andyylin@users.noreply.github.com": "andyylin",
"me@jakubkrcmar.cz": "jakubkrcmar",
"prasadus92@gmail.com": "prasadus92",
"michael@make.software": "mssteuer",
"der@konsi.org": "konsisumer",
"abogale2@gmail.com": "amanuel2",
"alexazzjjtt@163.com": "alexzhu0",
"pub_forgreatagent@antgroup.com": "AntAISecurityLab",
"252620095+briandevans@users.noreply.github.com": "briandevans",
"incharge.automation@gmail.com": "inchargeautomation-lab",
"danielrpike9@gmail.com": "Bartok9",
"96944678+ymylive@users.noreply.github.com": "sweetcornna",
"laflamme@illinoisalumni.org": "briancl2",
"skozyuk@cruxexperts.com": "CruxExperts",
"154585401+LeonSGP43@users.noreply.github.com": "LeonSGP43",
"12250313+Kailigithub@users.noreply.github.com": "Kailigithub",
"mgparkprint@gmail.com": "vlwkaos",
fix(gateway): snapshot callback generation after agent binds it, not before _process_message_background snapshotted callback_generation from the interrupt event at the TOP of the task — before the handler ran. _hermes_run_generation is only set on the event by GatewayRunner._bind_adapter_run_generation during _handle_message_with_agent, which runs DURING the handler await. The early snapshot always captured None, which then flowed into pop_post_delivery_callback(..., generation=None) in the finally block. In pop_post_delivery_callback, generation=None with a tuple-registered entry (generation, callback) bypasses the ownership check — it pops and fires the callback regardless of which run owns it. Result: a stale run could fire a fresher run's post-delivery callback (e.g. a background-review notification attributed to the wrong turn). Fix: move the snapshot into the finally block, after the handler has run and _hermes_run_generation has been bound to the current run. Regression test added: simulates a stale handler at generation=1 and a fresher callback registered at generation=2. Pre-fix: snapshot=None → pop fires the generation=2 callback under generation=1's ownership ("newer" fires). Post-fix: snapshot=1 → pop skips the mismatched entry, callback stays in the dict for the correct run to claim. Verified: test FAILS on current main (captures "newer" in fired list), PASSES with this fix. Salvaged from PR #12565 (the callback-ownership portion only; the /status totals portion was already fixed on main in 7abc9ce4d via #17158). Co-authored-by: Oxidane-bot <1317078257maroon@gmail.com>
2026-04-30 20:38:27 -07:00
"1317078257maroon@gmail.com": "Oxidane-bot",
"tranquil_flow@protonmail.com": "Tranquil-Flow",
fix(codex): surface actionable hint when stale-call detector fires on known silent-reject pattern The ChatGPT Codex backend (chatgpt.com/backend-api/codex) has historically silently dropped certain model requests: the connection is accepted but no stream events are emitted and no error is raised. PR #31967 lowered the implicit stale-call default from 300s to 90s so fallbacks kick in faster, but users still see an opaque "No response from provider for 90s (non-streaming, ...)" message that gives no path forward. This patch adds a narrow heuristic — gpt-5.5 family on the Codex backend via codex_responses api_mode — that substitutes the generic timeout message with actionable text naming the gpt-5.4-codex workaround and pointing at #21444 for symptom history. Changes: - run_agent.py — new ``AIAgent._codex_silent_hang_hint(model=...)`` method. Returns ``None`` for any request that does not match all three guards (codex_responses api_mode, openai-codex provider or chatgpt.com Codex base URL, gpt-5.5-family model name with word-boundary regex anchoring to avoid false-positives on e.g. ``gpt-5.50``). - agent/chat_completion_helpers.py — the non-stream stale-call site consults the hint via ``getattr(...)`` so the call site stays robust if the helper is ever removed or stubbed in tests. Hint is appended to both the ``_emit_status`` warning and the ``TimeoutError`` message so the user sees it in their terminal AND it lands in any retry-loop diagnostics. - tests/run_agent/test_codex_silent_hang_hint.py — 10 regression tests covering positive cases (bare gpt-5.5, vendor-prefixed openai/gpt-5.5, gpt-5.5-codex SKU, model=None fallback to self.model) and negative cases (gpt-5.4-codex workaround, gpt-5.50 false-positive guard, non-codex api_mode, non-codex provider, empty/None model, unrelated models on Codex). Does NOT fix the backend-side issue (that's an upstream OpenAI/ChatGPT problem we cannot patch from here). Only converts an opaque timeout into text that names the workaround so users do not have to dig through logs or wait for a forum post to learn what to do. Closes #22046
2026-05-25 03:54:07 -07:00
"66773372+Tranquil-Flow@users.noreply.github.com": "Tranquil-Flow",
"LyleLengyel@gmail.com": "mcndjxlefnd",
"wangshengyang2004@163.com": "Wangshengyang2004",
"hasan.ali13381@gmail.com": "H-Ali13381",
"xienb@proton.me": "XieNBi",
"139681654+maymuneth@users.noreply.github.com": "maymuneth",
"zengwei@nightq.cn": "nightq",
"1434494126@qq.com": "5park1e",
"158153005+5park1e@users.noreply.github.com": "5park1e",
"innocarpe@gmail.com": "innocarpe",
"noreply@ked.com": "qike-ms",
"andrekurait@gmail.com": "AndreKurait",
"bsgdigital@users.noreply.github.com": "bsgdigital",
"numman.ali@gmail.com": "nummanali",
"rohithsaimidigudla@gmail.com": "whitehatjr1001",
"0xNyk@users.noreply.github.com": "0xNyk",
"0xnykcd@googlemail.com": "0xNyk",
"buraysandro9@gmail.com": "buray",
"contact@jomar.fr": "joshmartinelle",
"camilo@tekelala.com": "tekelala",
"vincentcharlebois@gmail.com": "vincentcharlebois",
"aryan@synvoid.com": "aryansingh",
"johnsonblake1@gmail.com": "voteblake",
2026-04-17 19:17:34 -07:00
"hcn518@gmail.com": "pedh",
"haileymarshall005@gmail.com": "haileymarshall",
"bennet.yr.wang@gmail.com": "BennetYrWang",
"greer.guthrie@gmail.com": "g-guthrie",
"kennyx102@gmail.com": "bobashopcashier",
"77253505+bobashopcashier@users.noreply.github.com": "bobashopcashier",
"25355950+megastary@users.noreply.github.com": "megastary", # PR #18325
2026-04-14 16:55:25 -07:00
"shokatalishaikh95@gmail.com": "areu01or00",
"bryan@intertwinesys.com": "bryanyoung",
"christo.mitov@gmail.com": "christomitov",
"hermes@nousresearch.com": "NousResearch",
"reginaldasr@gmail.com": "ReginaldasR",
"ntconguit@gmail.com": "0xharryriddle",
"agent@wildcat.local": "ericnicolaides",
"georgex8001@gmail.com": "georgex8001",
"stefan@dimagents.ai": "dimitrovi",
fix(mcp-oauth): bidirectional auth_flow bridge + absolute expires_at (salvage #12025) (#12717) * [verified] fix(mcp-oauth): bridge httpx auth_flow bidirectional generator HermesMCPOAuthProvider.async_auth_flow wrapped the SDK's auth_flow with 'async for item in super().async_auth_flow(request): yield item', which discards httpx's .asend(response) values and resumes the inner generator with None. This broke every OAuth MCP server on the first HTTP response with 'NoneType' object has no attribute 'status_code' crashing at mcp/client/auth/oauth2.py:505. Replace with a manual bridge that forwards .asend() values into the inner generator, preserving httpx's bidirectional auth_flow contract. Add tests/tools/test_mcp_oauth_bidirectional.py with two regression tests that drive the flow through real .asend() round-trips. These catch the bug at the unit level; prior tests only exercised _initialize() and disk-watching, never the full generator protocol. Verified against BetterStack MCP: Before: 'Connection failed (11564ms): NoneType...' after 3 retries After: 'Connected (2416ms); Tools discovered: 83' Regression from #11383. * [verified] fix(mcp-oauth): seed token_expiry_time + pre-flight AS discovery on cold-load PR #11383's consolidation fixed external-refresh reloading and 401 dedup but left two latent bugs that surfaced on BetterStack and any other OAuth MCP with a split-origin authorization server: 1. HermesTokenStorage persisted only a relative 'expires_in', which is meaningless after a process restart. The MCP SDK's OAuthContext does NOT seed token_expiry_time in _initialize, so is_token_valid() returned True for any reloaded token regardless of age. Expired tokens shipped to servers, and app-level auth failures (e.g. BetterStack's 'No teams found. Please check your authentication.') were invisible to the transport-layer 401 handler. 2. Even once preemptive refresh did fire, the SDK's _refresh_token falls back to {server_url}/token when oauth_metadata isn't cached. For providers whose AS is at a different origin (BetterStack: mcp.betterstack.com for MCP, betterstack.com/oauth/token for the token endpoint), that fallback 404s and drops into full browser re-auth on every process restart. Fix set: - HermesTokenStorage.set_tokens persists an absolute wall-clock expires_at alongside the SDK's OAuthToken JSON (time.time() + TTL at write time). - HermesTokenStorage.get_tokens reconstructs expires_in from max(expires_at - now, 0), clamping expired tokens to zero TTL. Legacy files without expires_at fall back to file-mtime as a best-effort wall-clock proxy, self-healing on the next set_tokens. - HermesMCPOAuthProvider._initialize calls super(), then update_token_expiry on the reloaded tokens so token_expiry_time reflects actual remaining TTL. If tokens are loaded but oauth_metadata is missing, pre-flight PRM + ASM discovery runs via httpx.AsyncClient using the MCP SDK's own URL builders and response handlers (build_protected_resource_metadata_discovery_urls, handle_auth_metadata_response, etc.) so the SDK sees the correct token_endpoint before the first refresh attempt. Pre-flight is skipped when there are no stored tokens to keep fresh-install paths zero-cost. Test coverage (tests/tools/test_mcp_oauth_cold_load_expiry.py): - set_tokens persists absolute expires_at - set_tokens skips expires_at when token has no expires_in - get_tokens round-trips expires_at -> remaining expires_in - expired tokens reload with expires_in=0 - legacy files without expires_at fall back to mtime proxy - _initialize seeds token_expiry_time from stored tokens - _initialize flags expired-on-disk tokens as is_token_valid=False - _initialize pre-flights PRM + ASM discovery with mock transport - _initialize skips pre-flight when no tokens are stored Verified against BetterStack MCP: hermes mcp test betterstack -> Connected (2508ms), 83 tools mcp_betterstack_telemetry_list_teams_tool -> real team data, not 'No teams found. Please check your authentication.' Reference: mcp-oauth-token-diagnosis skill, Fix A. * chore: map hermes@noushq.ai to benbarclay in AUTHOR_MAP Needed for CI attribution check on cherry-picked commits from PR #12025. --------- Co-authored-by: Hermes Agent <hermes@noushq.ai>
2026-04-19 16:31:07 -07:00
"hermes@noushq.ai": "benbarclay",
2026-04-14 10:17:33 -07:00
"chinmingcock@gmail.com": "ChimingLiu",
"allard.quek@singtel.com": "AllardQuek",
"openclaw@sparklab.ai": "openclaw",
"semihcvlk53@gmail.com": "Himess",
"erenkar950@gmail.com": "erenkarakus",
"adavyasharma@gmail.com": "adavyas",
"acaayush1111@gmail.com": "aayushchaudhary",
"jason@outland.art": "jasonoutland",
feat(gateway): expose plugin slash commands natively on all platforms + decision-capable command hook Plugin slash commands now surface as first-class commands in every gateway enumerator — Discord native slash picker, Telegram BotCommand menu, Slack /hermes subcommand map — without a separate per-platform plugin API. The existing 'command:<name>' gateway hook gains a decision protocol via HookRegistry.emit_collect(): handlers that return a dict with {'decision': 'deny'|'handled'|'rewrite'|'allow'} can intercept slash command dispatch before core handling runs, unifying what would otherwise have been a parallel 'pre_gateway_command' hook surface. Changes: - gateway/hooks.py: add HookRegistry.emit_collect() that fires the same handler set as emit() but collects non-None return values. Backward compatible — fire-and-forget telemetry hooks still work via emit(). - hermes_cli/plugins.py: add optional 'args_hint' param to register_command() so plugins can opt into argument-aware native UI registration (Discord arg picker, future platforms). - hermes_cli/commands.py: add _iter_plugin_command_entries() helper and merge plugin commands into telegram_bot_commands() and slack_subcommand_map(). New is_gateway_known_command() recognizes both built-in and plugin commands so the gateway hook fires for either. - gateway/platforms/discord.py: extract _build_auto_slash_command helper from the COMMAND_REGISTRY auto-register loop and reuse it for plugin-registered commands. Built-in name conflicts are skipped. - gateway/run.py: before normal slash dispatch, call emit_collect on command:<canonical> and honor deny/handled/rewrite/allow decisions. Hook now fires for plugin commands too. - scripts/release.py: AUTHOR_MAP entry for @Magaav. - Tests: emit_collect semantics, plugin command surfacing per platform, decision protocol (deny/handled/rewrite/allow + non-dict tolerance), Discord plugin auto-registration + conflict skipping, is_gateway_known_command. Salvaged from #14131 (@Magaav). Original PR added a parallel 'pre_gateway_command' hook and a platform-keyed plugin command registry; this re-implementation reuses the existing 'command:<name>' hook and treats plugin commands as platform-agnostic so the same capability reaches Telegram and Slack without new API surface. Co-authored-by: Magaav <73175452+Magaav@users.noreply.github.com>
2026-04-22 15:01:50 -07:00
"73175452+Magaav@users.noreply.github.com": "Magaav",
"mrflu1918@proton.me": "SPANISHFLU",
"morganemoss@gmai.com": "mormio",
"kopjop926@gmail.com": "cesareth",
"fuleinist@gmail.com": "fuleinist",
"jack.47@gmail.com": "JackTheGit",
"jack@jackyang.com": "0xjackyang",
"dalvidjr2022@gmail.com": "Jr-kenny",
"m@statecraft.systems": "mbierling",
"balyan.sid@gmail.com": "alt-glitch",
feat(comfyui): add hardware check + auto-gate local install on verdict Layers a programmatic hardware-feasibility check on top of the v4 skill so the agent doesn't silently push users toward a local install they can't actually run. The official comfy-cli supports --nvidia / --amd / --m-series / --cpu, but has no guard against "4 GB laptop GPU on SDXL" or "Intel Mac falling back to CPU" — both route to comfy-cli paths in the original table and then fail on first workflow. - scripts/hardware_check.py: detect OS/arch/GPU (NVIDIA nvidia-smi, AMD rocm-smi, Apple M1+ via arm64+sysctl, Intel Arc via clinfo), VRAM, system/unified RAM. Emits JSON {verdict: ok|marginal|cloud, recommended_install_path, comfy_cli_flag} with practical thresholds: discrete GPU >=6 GB VRAM minimum, Apple Silicon >=16 GB unified memory minimum, Intel Mac -> cloud, no accelerator -> cloud. comfy_cli_flag maps directly to `comfy install` so the agent can stitch the whole flow together. - scripts/comfyui_setup.sh: runs hardware_check.py first when no explicit flag is passed. If verdict=cloud, refuses to install locally, prints Comfy Cloud URL + an override command, exits 2. Otherwise auto-selects the right --nvidia/--amd/--m-series flag for `comfy install`. Surfaces marginal-verdict notes to the user. - SKILL.md Setup & Onboarding: adds mandatory Step 0 "Check If This Machine Can Run ComfyUI Locally" ahead of the Path A-E selection. Documents the verdict thresholds inline, ties verdict + comfy_cli_flag to the install paths, and updates the path-choice table so "verdict: cloud" is the first row. Quick-Start "Detect Environment" block extended to include the hardware check. Verification checklist gains a hardware-check gate. - Frontmatter setup.help rewritten to point at hardware_check.py first. Version bumped 4.0.0 -> 4.1.0.
2026-04-29 12:38:09 -07:00
"52913345+alt-glitch@users.noreply.github.com": "alt-glitch",
"oluwadareab12@gmail.com": "oluwadareab12",
2026-04-14 20:47:57 -07:00
"simon@simonmarcus.org": "simon-marcus",
"xowiekk@gmail.com": "Xowiek",
2026-04-14 20:55:34 -07:00
"1243352777@qq.com": "zons-zhaozhy",
"e.silacandmr@gmail.com": "Es1la",
"51599529+stephen0110@users.noreply.github.com": "stephen0110",
"265632032+sonic-netizen@users.noreply.github.com": "sonic-netizen",
"82531659+mwnickerson@users.noreply.github.com": "mwnickerson",
2026-05-07 05:26:11 -07:00
"sandrohub013@gmail.com": "SandroHub013",
"maciekczech@users.noreply.github.com": "maciekczech",
"h3057183414@gmail.com": "CoreyNoDream",
"franksong2702@gmail.com": "franksong2702",
"673088860@qq.com": "ambition0802",
"beibei1988@proton.me": "beibi9966",
# ── bulk addition: 75 emails resolved via API, PR salvage bodies, noreply
# crossref, and GH contributor list matching (April 2026 audit) ──
fix(skills): make content_hash filename-sensitive too (symmetric with bundle_content_hash) PR #6656 added rel_path + \x00 prefixing to ``bundle_content_hash`` so a filename swap between two files in a bundle changes the digest. But it only patched the in-memory side — ``content_hash`` in ``tools/skills_guard.py`` (the on-disk equivalent) still hashed file contents only. These two functions need to stay symmetric: ``check_for_skill_updates`` compares the disk hash of an installed skill against the bundle hash of the upstream copy. With the asymmetric fix, every clean install showed as drifted because the digests no longer matched (2 existing tests in ``test_skills_hub.py`` started failing as soon as the contributor's change landed). Apply the same ``rel_path + \x00 + content`` shape to the disk-side function. Both functions now produce the same digest for the same skill content laid out two ways. Documented the symmetry invariant in the docstring so a future change to either function knows to touch both. Also adds tests/tools/test_pr_6656_regressions.py with 10 regression tests covering all three fixes salvaged in PR #6656: - uninstall_skill path traversal (4 cases: parent segments, absolute paths, symlink escape, legitimate skill) - bundle_content_hash filename swap detection (4 cases: in-memory swap, identity, disk-side swap, bundle↔disk symmetry) - list_pending lock contract (2 cases: source-grep contract, smoke) Also fixes AUTHOR_MAP entry for @aaronlab — their commit email (1115117931@qq.com) maps to "aaronagent" which isn't a real GitHub login, so changelog @mentions would 404.
2026-05-22 19:55:58 -07:00
"1115117931@qq.com": "aaronlab",
"1506751656@qq.com": "hqhq1025",
"364939526@qq.com": "luyao618",
"hgk324@gmail.com": "houziershi",
"176644217+PStarH@users.noreply.github.com": "PStarH",
"51058514+Sanjays2402@users.noreply.github.com": "Sanjays2402",
"16577466+andy825@user.noreply.gitee.com": "Andy283",
"906014227@qq.com": "bingo906",
"aaronwong1999@icloud.com": "AaronWong1999",
"agents@kylefrench.dev": "DeployFaith",
"angelos@oikos.lan.home.malaiwah.com": "angelos",
"aptx4561@gmail.com": "cokemine",
"arilotter@gmail.com": "ethernet8023",
"ben@nousresearch.com": "benbarclay",
"birdiegyal@gmail.com": "yyovil",
"boschi1997@gmail.com": "nicoloboschi",
"chef.ya@gmail.com": "cherifya",
"chlqhdtn98@gmail.com": "BongSuCHOI",
"coffeemjj@gmail.com": "Cafexss",
"dalianmao0107@gmail.com": "dalianmao000",
"der@konsi.org": "konsisumer",
"dgrieco@redhat.com": "DomGrieco",
"dhicham.pro@gmail.com": "spideystreet",
"dipp.who@gmail.com": "dippwho",
"don.rhm@gmail.com": "donrhmexe",
"dorukardahan@hotmail.com": "dorukardahan",
"dsocolobsky@gmail.com": "dsocolobsky",
"dylan.socolobsky@lambdaclass.com": "dsocolobsky",
"ignacio.avecilla@lambdaclass.com": "IAvecilla",
"duerzy@gmail.com": "duerzy",
"emozilla@nousresearch.com": "emozilla",
"fancydirty@gmail.com": "fancydirty",
"farion1231@gmail.com": "farion1231",
"floptopbot33@gmail.com": "flobo3",
"fontana.pedro93@gmail.com": "pefontana",
"francis.x.fitzpatrick@gmail.com": "fxfitz",
"frank@helmschrott.de": "Helmi",
"gaixg94@gmail.com": "gaixianggeng",
"geoff.wellman@gmail.com": "geoffwellman",
"han.shan@live.cn": "jamesarch",
"haolong@microsoft.com": "LongOddCode",
"glennc@microsoft.com": "glennc",
"hata1234@gmail.com": "hata1234",
"hmbown@gmail.com": "Hmbown",
"iacobs@m0n5t3r.info": "m0n5t3r",
"jiayuw794@gmail.com": "JiayuuWang",
"jonny@nousresearch.com": "yoniebans",
"jake@nousresearch.com": "simpolism",
"juan.ovalle@mistral.ai": "jjovalle99",
"julien.talbot@ergonomia.re": "Julientalbot",
"kagura.chen28@gmail.com": "kagura-agent",
"1342088860@qq.com": "youngDoo",
"kamil@gwozdz.me": "kamil-gwozdz",
"skmishra1991@gmail.com": "bugkill3r",
"karamusti912@gmail.com": "MustafaKara7",
"kira@ariaki.me": "kira-ariaki",
"kira.ops@proton.me": "KiraKatana",
"knopki@duck.com": "knopki",
"limars874@gmail.com": "limars874",
"lisicheng168@gmail.com": "lesterli",
"mingjwan@microsoft.com": "MagicRay1217",
2026-04-16 05:58:52 -07:00
"orangeko@gmail.com": "GenKoKo",
2026-04-16 06:47:42 -07:00
"82095453+iacker@users.noreply.github.com": "iacker",
"sontianye@users.noreply.github.com": "sontianye",
"jackjin1997@users.noreply.github.com": "jackjin1997",
"1037461232@qq.com": "jackjin1997",
"danieldoderlein@users.noreply.github.com": "danieldoderlein",
"lrawnsley@users.noreply.github.com": "lrawnsley",
"taeuk178@users.noreply.github.com": "taeuk178",
"ogzerber@users.noreply.github.com": "ogzerber",
"cola-runner@users.noreply.github.com": "cola-runner",
"ygd58@users.noreply.github.com": "ygd58",
"45554392+warabe1122@users.noreply.github.com": "warabe1122",
"187001140+willy-scr@users.noreply.github.com": "willy-scr",
"vominh1919@users.noreply.github.com": "vominh1919",
"iamagenius00@users.noreply.github.com": "iamagenius00",
"9219265+cresslank@users.noreply.github.com": "cresslank",
"trevmanthony@gmail.com": "trevthefoolish",
"ziliangpeng@users.noreply.github.com": "ziliangpeng",
"ziliangdotme@gmail.com": "ziliangpeng",
"centripetal-star@users.noreply.github.com": "centripetal-star",
"LeonSGP43@users.noreply.github.com": "LeonSGP43",
"154585401+LeonSGP43@users.noreply.github.com": "LeonSGP43",
"cine.dreamer.one@gmail.com": "LeonSGP43",
"Lubrsy706@users.noreply.github.com": "Lubrsy706",
"niyant@spicefi.xyz": "spniyant",
"olafthiele@gmail.com": "olafthiele",
"oncuevtv@gmail.com": "sprmn24",
"programming@olafthiele.com": "olafthiele",
"r2668940489@gmail.com": "r266-tech",
"s5460703@gmail.com": "BlackishGreen33",
"saul.jj.wu@gmail.com": "SaulJWu",
"shenhaocheng19990111@gmail.com": "hcshen0111",
"sjtuwbh@gmail.com": "Cygra",
"srhtsrht17@gmail.com": "Sertug17",
"stephenschoettler@gmail.com": "stephenschoettler",
"tanishq231003@gmail.com": "yyovil",
"taosiyuan163@153.com": "taosiyuan163",
"tesseracttars@gmail.com": "tesseracttars-creator",
"tianliangjay@gmail.com": "xingkongliang",
fix(gateway): snapshot callback generation after agent binds it, not before _process_message_background snapshotted callback_generation from the interrupt event at the TOP of the task — before the handler ran. _hermes_run_generation is only set on the event by GatewayRunner._bind_adapter_run_generation during _handle_message_with_agent, which runs DURING the handler await. The early snapshot always captured None, which then flowed into pop_post_delivery_callback(..., generation=None) in the finally block. In pop_post_delivery_callback, generation=None with a tuple-registered entry (generation, callback) bypasses the ownership check — it pops and fires the callback regardless of which run owns it. Result: a stale run could fire a fresher run's post-delivery callback (e.g. a background-review notification attributed to the wrong turn). Fix: move the snapshot into the finally block, after the handler has run and _hermes_run_generation has been bound to the current run. Regression test added: simulates a stale handler at generation=1 and a fresher callback registered at generation=2. Pre-fix: snapshot=None → pop fires the generation=2 callback under generation=1's ownership ("newer" fires). Post-fix: snapshot=1 → pop skips the mismatched entry, callback stays in the dict for the correct run to claim. Verified: test FAILS on current main (captures "newer" in fired list), PASSES with this fix. Salvaged from PR #12565 (the callback-ownership portion only; the /status totals portion was already fixed on main in 7abc9ce4d via #17158). Co-authored-by: Oxidane-bot <1317078257maroon@gmail.com>
2026-04-30 20:38:27 -07:00
"1317078257maroon@gmail.com": "Oxidane-bot",
"tranquil_flow@protonmail.com": "Tranquil-Flow",
"LyleLengyel@gmail.com": "mcndjxlefnd",
"unayung@gmail.com": "Unayung",
"vorvul.danylo@gmail.com": "WorldInnovationsDepartment",
"win4r@outlook.com": "win4r",
"xush@xush.org": "KUSH42",
"yangzhi.see@gmail.com": "SeeYangZhi",
"yongtenglei@gmail.com": "yongtenglei",
"young@YoungdeMacBook-Pro.local": "YoungYang963",
"ysfalweshcan@gmail.com": "Junass1",
"ysfwaxlycan@gmail.com": "WAXLYY",
"yusufalweshdemir@gmail.com": "Dusk1e",
"zhouboli@gmail.com": "zhouboli",
"zqiao@microsoft.com": "tomqiaozc",
"zzn+pa@zzn.im": "xinbenlv",
2026-04-15 02:33:56 +08:00
"zaynjarvis@gmail.com": "ZaynJarvis",
"zhiheng.liu@bytedance.com": "ZaynJarvis",
"izhaolongfei@gmail.com": "loongfay",
"296659110@qq.com": "lrt4836",
"fe.daniel91@gmail.com": "beforeload",
"libo1106@foxmail.com": "libo1106",
"295367131@qq.com": "295367131",
"295367132@qq.com": "IxAres",
"danieldliu@tencent.com": "danieldliu",
"loongzhao@tencent.com": "loongzhao",
"Bartok9@users.noreply.github.com": "Bartok9",
"LeonSGP43@users.noreply.github.com": "LeonSGP43",
"kshitijk4poor@users.noreply.github.com": "kshitijk4poor",
"mbelleau@Michels-MacBook-Pro.local": "malaiwah",
"michel.belleau@malaiwah.com": "malaiwah",
"gnanasekaran.sekareee@gmail.com": "gnanam1990",
"jz.pentest@gmail.com": "0xyg3n",
"ian@culling.ca": "ianculling", # PR #36087
"7093928+0xyg3n@users.noreply.github.com": "0xyg3n",
2026-05-03 16:17:09 +05:30
"nftpoetrist@gmail.com": "nftpoetrist", # PR #18982
2026-05-03 16:18:47 +05:30
"millerc79@users.noreply.github.com": "millerc79", # PR #19033
"hermes@example.com": "shellybotmoyer", # PR #18915 (bot-committed)
"exx@example.com": "exxmen", # PR #19555
"hypnosis.mda@gmail.com": "Hypn0sis",
"ywt000818@gmail.com": "OwenYWT",
"dhandhalyabhavik@gmail.com": "v1k22",
"rucchizhao@zhaochenfeideMacBook-Pro.local": "RucchiZ",
"tannerfokkens@Mac.attlocal.net": "tannerfokkens-maker",
"lehaolin98@outlook.com": "LehaoLin",
"yuewang1@microsoft.com": "imink",
"1736355688@qq.com": "hedgeho9X",
"bernylinville@devopsthink.org": "bernylinville",
"brian@bde.io": "briandevans",
"hubin_ll@qq.com": "LLQWQ",
"memosr_email@gmail.com": "memosr",
"jperlow@gmail.com": "perlowja",
"jasonpette1783@gmail.com": "web-dev0521",
"bjianhang@gmail.com": "bjianhang",
"tangyuanjc@JCdeAIfenshendeMac-mini.local": "tangyuanjc",
"harryplusplus@gmail.com": "harryplusplus",
"anthhub@163.com": "anthhub",
"vmphuongit@gmail.com": "phuongvm",
"allard.quek@singtel.com": "AllardQuek",
"shenuu@gmail.com": "shenuu",
"xiayh17@gmail.com": "xiayh0107",
"zhujianxyz@gmail.com": "opriz",
"tuancanhnguyen706@gmail.com": "xxxigm",
"larcombe.n@gmail.com": "NickLarcombe",
"54813621+xxxigm@users.noreply.github.com": "xxxigm",
fix(providers): complete NVIDIA NIM parity with other providers Follow-up on the native NVIDIA NIM provider salvage. The original PR wired PROVIDER_REGISTRY + HERMES_OVERLAYS correctly but missed several touchpoints required for full parity with other OpenAI-compatible providers (xai, huggingface, deepseek, zai). Gaps closed: - hermes_cli/main.py: - Add 'nvidia' to the _model_flow_api_key_provider dispatch tuple so selecting 'NVIDIA NIM' in `hermes model` actually runs the api-key provider flow (previously fell through silently). - Add 'nvidia' to `hermes chat --provider` argparse choices so the documented test command (`hermes chat --provider nvidia --model ...`) parses successfully. - hermes_cli/config.py: Register NVIDIA_API_KEY and NVIDIA_BASE_URL in OPTIONAL_ENV_VARS so setup wizard can prompt for them and they're auto-added to the subprocess env blocklist. - hermes_cli/doctor.py: Add NVIDIA NIM row to `_apikey_providers` so `hermes doctor` probes https://integrate.api.nvidia.com/v1/models. - hermes_cli/dump.py: Add NVIDIA_API_KEY → 'nvidia' mapping for `hermes dump` credential masking. - tests/tools/test_local_env_blocklist.py: Extend registry_vars fixture with NVIDIA_API_KEY to verify it's blocked from leaking into subprocesses. - agent/model_metadata.py: Add 'nemotron' → 131072 context-length entry so all Nemotron variants get 128K context via substring match (rather than falling back to MINIMUM_CONTEXT_LENGTH). - hermes_cli/models.py: Fix hallucinated model ID 'nvidia/nemotron-3-nano-8b-a4b' → 'nvidia/nemotron-3-nano-30b-a3b' (verified against live integrate.api.nvidia.com/v1/models catalog). Expand curated list from 5 to 9 agentic models mapping to OpenRouter defaults per provider-guide convention: add qwen3.5-397b-a17b, deepseek-v3.2, llama-3.3-nemotron-super-49b-v1.5, gpt-oss-120b. - cli-config.yaml.example: Document 'nvidia' provider option. - scripts/release.py: Map asurla@nvidia.com → anniesurla in AUTHOR_MAP for CI attribution. E2E verified: `hermes chat --provider nvidia ...` now reaches NVIDIA's endpoint (returns 401 with bogus key instead of argparse error); `hermes doctor` detects NVIDIA NIM when NVIDIA_API_KEY is set.
2026-04-17 13:09:14 -07:00
"asurla@nvidia.com": "anniesurla",
"kchantharuan@nvidia.com": "nv-kasikritc",
"bbednarski@nvidia.com": "bbednarski9",
2026-04-17 20:31:47 +08:00
"limkuan24@gmail.com": "WideLee",
"aviralarora002@gmail.com": "AviArora02-commits",
"draixagent@gmail.com": "draix",
"martin.alca@gmail.com": "draix",
"junminliu@gmail.com": "JimLiu",
"juraj@bednar.io": "jooray",
"jarvischer@gmail.com": "maxchernin",
"levantam.98.2324@gmail.com": "LVT382009",
"zhurongcheng@rcrai.com": "heykb",
"withapurpose37@gmail.com": "StefanIsMe",
"261797239+lumenradley@users.noreply.github.com": "lumenradley",
"166376523+sjz-ks@users.noreply.github.com": "sjz-ks",
"haileymarshall005@gmail.com": "haileymarshall",
"aniruddhaadak80@users.noreply.github.com": "aniruddhaadak80",
"zheng.jerilyn@gmail.com": "jerilynzheng",
fix: extend hostname-match provider detection across remaining call sites Aslaaen's fix in the original PR covered _detect_api_mode_for_url and the two openai/xai sites in run_agent.py. This finishes the sweep: the same substring-match false-positive class (e.g. https://api.openai.com.evil/v1, https://proxy/api.openai.com/v1, https://api.anthropic.com.example/v1) existed in eight more call sites, and the hostname helper was duplicated in two modules. - utils: add shared base_url_hostname() (single source of truth). - hermes_cli/runtime_provider, run_agent: drop local duplicates, import from utils. Reuse the cached AIAgent._base_url_hostname attribute everywhere it's already populated. - agent/auxiliary_client: switch codex-wrap auto-detect, max_completion_tokens gate (auxiliary_max_tokens_param), and custom-endpoint max_tokens kwarg selection to hostname equality. - run_agent: native-anthropic check in the Claude-style model branch and in the AIAgent init provider-auto-detect branch. - agent/model_metadata: Anthropic /v1/models context-length lookup. - hermes_cli/providers.determine_api_mode: anthropic / openai URL heuristics for custom/unknown providers (the /anthropic path-suffix convention for third-party gateways is preserved). - tools/delegate_tool: anthropic detection for delegated subagent runtimes. - hermes_cli/setup, hermes_cli/tools_config: setup-wizard vision-endpoint native-OpenAI detection (paired with deduping the repeated check into a single is_native_openai boolean per branch). Tests: - tests/test_base_url_hostname.py covers the helper directly (path-containing-host, host-suffix, trailing dot, port, case). - tests/hermes_cli/test_determine_api_mode_hostname.py adds the same regression class for determine_api_mode, plus a test that the /anthropic third-party gateway convention still wins. Also: add asslaenn5@gmail.com → Aslaaen to scripts/release.py AUTHOR_MAP.
2026-04-20 20:58:01 -07:00
"asslaenn5@gmail.com": "Aslaaen",
"shalompmc0505@naver.com": "pinion05",
2026-04-20 12:54:48 +09:00
"105142614+VTRiot@users.noreply.github.com": "VTRiot",
"vivien000812@gmail.com": "iamagenius00",
"89228157+Feranmi10@users.noreply.github.com": "Feranmi10",
"oluwadareferanmi11@gmail.com": "Feranmi10",
"simon@gtcl.us": "simon-gtcl",
"suzukaze.haduki@gmail.com": "houko",
"cliff@cigii.com": "cgarwood82",
"anna@oa.ke": "anna-oake",
"jaffarkeikei@gmail.com": "jaffarkeikei",
"hxp@hxp.plus": "hxp-plus",
"3580442280@qq.com": "Tianworld",
"wujianxu91@gmail.com": "wujhsu",
"zhrh120@gmail.com": "niyoh120",
"vrinek@hey.com": "vrinek",
"268198004+xandersbell@users.noreply.github.com": "xandersbell",
"somme4096@gmail.com": "Somme4096",
"brian@tiuxo.com": "brianclemens",
"25944632+yudaiyan@users.noreply.github.com": "yudaiyan",
"chayton@sina.com": "ycbai",
"longsizhuo@gmail.com": "longsizhuo",
"chenb19870707@gmail.com": "ms-alan",
"agorgianitisj@hotmail.com": "johnisag",
"phil.thomas@gametime.co": "explainanalyze",
"276886827+WuTianyi123@users.noreply.github.com": "WuTianyi123",
"22549957+li0near@users.noreply.github.com": "li0near",
"guoyu801@gmail.com": "li0near",
"ty@tmrtn.com": "tymrtn",
"elitovsky@zenproject.net": "kallidean",
"5463986+baocin@users.noreply.github.com": "baocin",
"107296821+princepal9120@users.noreply.github.com": "princepal9120",
"gufo0125@gmail.com": "guglielmofonda",
refactor(kanban-orchestrator): drop hardcoded specialist roster, add Step-0 profile discovery The skill enumerated 8 specialist profile names (researcher, analyst, writer, reviewer, backend-eng, frontend-eng, ops, pm) as "the standard roster" and told orchestrators to "assume these exist." Almost no real Hermes setup matches that fleet — single-profile setups, Docker-worker setups, and curated-team setups all violate it — so following the skill literally produced cards assigned to non-existent profiles, which the dispatcher silently failed to spawn (no autocorrect, no fallback, just sits in `ready` forever). Changes: - Drop the standard-specialist-roster table. - Add a "Profiles are user-configured — not a fixed roster" section at the top with a Step 0 that prescribes `hermes profile list` (or asking the user) before fanning out. Cache the result in working memory. - Rewrite the worked task-graph example with placeholder names (<profile-A>, <profile-B>, <profile-C>) so the structure is still teachable but doesn't invite copy-paste of role names that may not exist. - Reframe the "If no specialist fits" anti-temptation rule: don't invent profile names; ask the user. - Add a "Inventing profile names that doesn't exist" entry to Pitfalls. - Bump skill version 2.0.0 → 3.0.0 (semantic break: previous behavior promised a roster the skill no longer enumerates). - Update website/docs/user-guide/features/kanban.md to drop the matching "(researcher, writer, analyst, backend-eng, reviewer, ops)" line and explain the discovery prompt instead. - Re-run website/scripts/generate-skill-docs.py to refresh the auto-generated skill page + catalog. Closes #21131 in spirit — addresses the same hardcoded-names footgun @yehuosi flagged, with a different shape than their PR (delete the roster rather than replace each name with placeholder, since the roster table was the load-bearing footgun and the worked example is salvageable with placeholder profile names). Co-authored-by: yehuosi <yehuosi@users.noreply.github.com>
2026-05-10 12:58:33 -07:00
"102474490+yehuosi@users.noreply.github.com": "yehuosi",
"yehuosi@users.noreply.github.com": "yehuosi",
"31932854+jelrod27@users.noreply.github.com": "jelrod27",
"11262660+konsisumer@users.noreply.github.com": "konsisumer",
"23434080+sicnuyudidi@users.noreply.github.com": "sicnuyudidi",
"haimu0x0@proton.me": "haimu0x",
"abdelmajidnidnasser1@gmail.com": "NIDNASSER-Abdelmajid",
"projectadmin@wit.id": "projectadmin-dev",
"mrigankamondal10@gmail.com": "Dev-Mriganka",
"132275809+shushuzn@users.noreply.github.com": "shushuzn",
"ibrahimozsarac@gmail.com": "iborazzi",
"130149563+A-afflatus@users.noreply.github.com": "A-afflatus",
"huangkwell@163.com": "huangke19",
"tanishq@exa.ai": "10ishq",
"363708+christopherwoodall@users.noreply.github.com": "christopherwoodall",
"zhang9w0v5@qq.com": "zhang9w0v5",
"fuleinist@outlook.com": "fuleinist",
"43494187+Llugaes@users.noreply.github.com": "Llugaes",
"xiangji.chen@centurygame.com": "Llugaes",
"fengtianyu88@users.noreply.github.com": "fengtianyu88",
"l.moncany@gmail.com": "lmoncany",
"fatinghenji@users.noreply.github.com": "fatinghenji",
"xin.peng.dr@gmail.com": "xinpengdr",
"mike@mikewaters.net": "mikewaters",
"65117428+WadydX@users.noreply.github.com": "WadydX",
"216480837+isaachuangGMICLOUD@users.noreply.github.com": "isaachuangGMICLOUD",
2026-05-06 14:23:59 -07:00
"isaac.h@gmicloud.ai": "isaachuangGMICLOUD",
"nukuom976228@gmail.com": "hsy5571616",
"11462216+Nan93@users.noreply.github.com": "Nan93",
"l973401489@126.com": "zhouxiaoya12",
"373119611@qq.com": "roytian1217",
"brett@brettbrewer.com": "minorgod",
"67779267+wenhao7@users.noreply.github.com": "wenhao7",
2026-04-23 02:06:02 -07:00
"git@yzx9.xyz": "yzx9",
"nilesh@cloudgeni.us": "lvnilesh",
"63502660+azhengbot@users.noreply.github.com": "azhengbot",
"sharvil.saxena@gmail.com": "sharziki",
"yuanhe@minimaxi.com": "RyanLee-Dev",
"curtis992250@gmail.com": "TaroballzChen",
"92638503+Lind3ey@users.noreply.github.com": "Lind3ey",
"1352808998@qq.com": "phpoh",
"caliberoviv@gmail.com": "vivganes",
"michaelfackerell@gmail.com": "MikeFac",
"18024642@qq.com": "GuyCui",
"eumael.mkt@gmail.com": "maelrx",
# v0.11.0 additions
"benbarclay@gmail.com": "benbarclay",
"lijiawen@umich.edu": "Jiawen-lee",
"oleksiy@kovyrin.net": "kovyrin",
"kovyrin.claw@gmail.com": "kovyrin",
"kaiobarb@gmail.com": "liftaris",
"me@arihantsethia.com": "arihantsethia",
"zhuofengwang2003@gmail.com": "coekfung",
"teknium@noreply.github.com": "teknium1",
"2114364329@qq.com": "cuyua9",
"2557058999@qq.com": "Disaster-Terminator",
"cine.dreamer.one@gmail.com": "LeonSGP43",
"zyprothh@gmail.com": "Zyproth",
"amitgaur@gmail.com": "amitgaur",
"albuquerque.abner@gmail.com": "mrbob-git",
"kiala@users.noreply.github.com": "kiala9",
"alanxchen@gmail.com": "alanxchen85",
"clawbot@clawbots-Mac-mini.local": "John-tip",
"der@konsi.org": "konsisumer",
"cirwel@The-CIRWEL-Group.local": "CIRWEL",
"molvikar8@gmail.com": "molvikar",
"nftpoetrist@gmail.com": "nftpoetrist",
"dodofun@126.com": "colorcross",
"1615063567@qq.com": "zhao0112",
"ethanguo.2003@gmail.com": "EthanGuo-coder",
"dev0jsh@gmail.com": "tmdgusya",
"leavr@163.com": "leavrcn",
"17683456+wanazhar@users.noreply.github.com": "wanazhar",
"26782336+cixuuz@users.noreply.github.com": "cixuuz",
"aleksandr.pasevin@openzeppelin.com": "pasevin",
"ubuntu@localhost.localdomain": "holynn-q",
"holynn@placeholder.local": "holynn-q",
"agent@hermes.local": "jacdevos",
"sunsky.lau@gmail.com": "liuhao1024",
"rob@rbrtbn.com": "rbrtbn",
"haaasined@gmail.com": "VinciZhu",
"fabianoeq@gmail.com": "rodrigoeqnit",
"178342791+sgtworkman@users.noreply.github.com": "sgtworkman",
"qiuqfang98@qq.com": "keepcalmqqf",
"261867348+ai-ag2026@users.noreply.github.com": "ai-ag2026",
"yanzh.su@gmail.com": "YanzhongSu",
"wanderwang@users.noreply.github.com": "WanderWang",
"yueheime@gmail.com": "yuehei",
"emidomh@gmail.com": "Emidomenge",
"2642448440@qq.com": "BlackJulySnow",
"4317663+helix4u@users.noreply.github.com": "helix4u",
"floptopbot33@gmail.com": "flobo3",
"dpaluy@users.noreply.github.com": "dpaluy",
"psikonetik@gmail.com": "el-analista",
"chenb19870707@gmail.com": "ms-alan",
"agorgianitisj@hotmail.com": "johnisag",
"phil.thomas@gametime.co": "explainanalyze",
"hex-clawd@users.noreply.github.com": "hex-clawd",
"154585401+LeonSGP43@users.noreply.github.com": "LeonSGP43",
"barteq@hacknotes.local": "barteqpl",
"pama0227@gmail.com": "pama0227",
"52785845+ee-blog@users.noreply.github.com": "ee-blog",
"simplenamebox@gmail.com": "simplenamebox-ops",
"balyan.sid@gmail.com": "alt-glitch",
"xdord@xdorddeMac-mini.local": "foreverxdord",
"k2767567815@gmail.com": "QifengKuang",
"88077783+jjjojoj@users.noreply.github.com": "jjjojoj",
"valda@underscore.jp": "valda",
"lling486@163.com": "M3RCUR2Y",
"buraysandro9@gmail.com": "ygd58",
"ideathinklab01-source@users.noreply.github.com": "ideathinklab01-source",
"27987889@qq.com": "zng8418",
"daniuxie88@proton.me": "DaniuXie",
"panchanler@gmail.com": "ChanlerDev",
"252620095+briandevans@users.noreply.github.com": "briandevans",
"141889580+h0tp-ftw@users.noreply.github.com": "h0tp-ftw",
"chinadbo@foxmail.com": "chinadbo",
"82637225+kshitijk4poor@users.noreply.github.com": "kshitijk4poor",
"xyywtt@gmail.com": "xyiy001",
"charliekerfoot@gmail.com": "CharlieKerfoot",
"grey0202@users.noreply.github.com": "Grey0202",
"vominh1919@gmail.com": "vominh1919",
"giwavictor9@gmail.com": "giwaov",
"yoimexex@gmail.com": "Yoimex",
"76803960+atongrun@users.noreply.github.com": "atongrun",
"michaeldanko@icloud.com": "MichaelWDanko",
"xudavid429@gmail.com": "YX234",
"kathy@Kathy.local": "julysir",
"274902531@qq.com": "JanCong",
"225304168+e-shizz@users.noreply.github.com": "e-shizz",
"vincent_hh@users.noreply.github.com": "VinVC",
"1243352777@qq.com": "zons-zhaozhy",
"dejie.guo@gmail.com": "JayGwod",
"52840391+swithek@users.noreply.github.com": "swithek",
"raipratik0101@gmail.com": "PratikRai0101",
"code@sasha.id": "sasha-id",
"chen.yunbo@xydigit.com": "chenyunbo411",
"openclaw@local": "Asce66",
"59465365+0xsir0000@users.noreply.github.com": "0xsir0000",
"lisanhu2014@hotmail.com": "lisanhu",
"0668001438@zte.com.cn": "chenyunbo411",
"steven_chanin@alum.mit.edu": "stevenchanin",
"fiver@example.com": "halmisen",
"mayq0422@gmail.com": "yuqianma",
"yuqian@zmetasoft.com": "yuqianma",
"scott@bubble.local": "bassings",
"highland0971@users.noreply.github.com": "highland0971",
"sudolewis@gmail.com": "lewislulu",
"gaurav2301v@gmail.com": "Gaurav23V",
"tranquil_flow@protonmail.com": "Tranquil-Flow",
"albert748@gmail.com": "albert748",
"ntconguit@gmail.com": "0xharryriddle",
"lhysdl@gmail.com": "lhysdl",
"shemol@163.com": "SherlockShemol",
"enochlam2002@gmail.com": "eloklam",
"eloklam@eloklam-ubuntudesktop.tail21966c.ts.net": "eloklam",
"clawdia@fmercurio-macstudio.local": "fmercurio",
"ricardoporsche001@icloud.com": "Ricardo-M-L",
"leozeli@qq.com": "leozeli",
"linlehao@cuhk.edu.cn": "LehaoLin",
"liutong@isacas.ac.cn": "I3eg1nner",
"peterberthelsen@Peters-MacBook-Air.local": "PeterBerthelsen",
"root@debian.debian": "lengxii",
"roque@priveperfumeshn.com": "priveperfumes",
"shijianzhi@shijianzhideMacBook-Pro.local": "sjz-ks",
"topcheer@me.com": "topcheer",
"walli@tencent.com": "walli",
"zhuofengwang@tencent.com": "Zhuofeng-Wang",
"simonweng@tencent.com": "Contentment003111",
# April 2026 salvage-PR batch (#14920, #14986, #14966)
"mrunmayeerane17@gmail.com": "mrunmayee17",
"69489633+camaragon@users.noreply.github.com": "camaragon",
"shamork@outlook.com": "shamork",
# April 2026 Discord Copilot /model salvage (#15030)
"cshong2017@outlook.com": "Nicecsh",
# no-github-match — keep as display names
"clio-agent@sisyphuslabs.ai": "Sisyphus",
"marco@rutimka.de": "Marco Rutsch",
"paul@gamma.app": "Paul Bergeron",
"zhangxicen@example.com": "zhangxicen",
"codex@openai.invalid": "teknium1",
"screenmachine@gmail.com": "teknium1",
"chenzeshi@live.com": "chen1749144759",
"mor.aleksandr@yahoo.com": "MorAlekss",
"276649498+ztexydt-cqh@users.noreply.github.com": "ztexydt-cqh",
# v0.16.0 additions
"teknium@nous.dev": "teknium1",
"alaamohanad169@gmail.com": "alaamohanad169-ship-it",
"archer@ouyangdeMac-mini.local": "Archerouyang", # display name 欧阳
"batosk2@gmail.com": "Sarbai", # git email for PR #33438 author (display: Брагарник Дмитро)
"info@aminvakil.com": "aminvakil",
"nikpolale@gmail.com": "polnikale",
"sarveshagl1327@gmail.com": "sarvesh1327", # salvaged via #38655
"sohyuanchin@gmail.com": "wysie",
"bedirhan@codeway.co": "bedirhancode",
"ash@users.noreply.github.com": "ash",
"andrewho.sf@gmail.com": "andrewhosf",
# April 2026 Honcho bug-fix consolidation (#15381)
"HiddenPuppy@users.noreply.github.com": "HiddenPuppy",
"code@sasha.id": "sasha-id",
"dontcallmejames@users.noreply.github.com": "dontcallmejames",
"hekaru.agent@gmail.com": "hekaru-agent",
"jas9000@gmail.com": "twozle",
"r.filgueiras@apheris.com": "rfilgueiras",
"leihaibo1992@gmail.com": "Leihb",
# ACP streaming fix salvage (PR #9428 + #16273)
"nfb0408@163.com": "ningfangbin",
"164839249+Joseph19820124@users.noreply.github.com": "Joseph19820124",
"rugved@lmstudio.ai": "rugvedS07",
"44333070+Heltman@users.noreply.github.com": "Heltman",
# v0.12.0 additions
"ching@kachingappz.com": "ching-kaching",
"codezhujr@gmail.com": "Zjianru", # salvage chain: code by codez, PR #15749 author @Zjianru
"daimon@noreply.github.com": "Siddharth Balyan", # co-author only
"i@zkl2333.com": "zkl2333",
"isaachuang@Isaacs-MacBook-Pro.local": "isaachuangGMICLOUD",
"isaachuang@Mac.localdomain": "isaachuangGMICLOUD", # salvage of PR #11955 → #16663
"liyuan851277048@icloud.com": "Octopus", # co-author only
"me+github7604@versun.org": "Versun", # co-author only
"my.vesper.nine@gmail.com": "kevin-ho", # salvage: PR #15488 author @kevin-ho
"noreply@paperclip.ing": "Paperclip", # co-author only
"teknium@hermes-agent": "teknium1",
"web3blind@gmail.com": "web3blind",
"ztzheng@163.com": "chengoak", # PR #17467
"zwcf5200@163.com": "zwcf5200", # PR #38661 (SSH remote cwd fix)
"24110240104@m.fudan.edu.cn": "YuShu", # co-author only
"charliekerfoot@gmail.com": "CharlieKerfoot", # PR #18951
# Debug share upload-time redaction (May 2026)
"dhuysamen@gmail.com": "GodsBoy", # PR #19318
"github@nadyahermes.anonaddy.com": "ruangraung", # PR #42308
"mrcoferland@gmail.com": "mrcoferland", # PR #19023
"chenlinfeng@ruije.com.cn": "noOne-list", # PR #19050
2026-05-07 05:10:58 -07:00
"briansu@Mac-mini.attlocal.net": "likejudy", # PR #19052
2026-05-07 05:19:58 -07:00
"leosma@gmail.com": "leon7609", # PR #19069
"nouseman666@gmail.com": "nouseman666", # PR #19088
2026-05-07 05:26:18 -07:00
"ginwu05@gmail.com": "GinWU05", # PR #19093
"shashwatgokhe2@gmail.com": "shashwatgokhe", # PR #19196
"stevenchou.ai@gmail.com": "stevenchouai", # PR #19221
2026-05-07 06:19:46 -07:00
"leo.gong@phizchat.com": "agilejava", # PR #19346
2026-05-07 06:21:39 -07:00
"acc001k@pm.me": "acc001k", # PR #19358
"kowenhao@users.noreply.github.com": "kowenhaoai", # PR #19376
2026-05-07 06:27:34 -07:00
"hedirman@gmail.com": "hedirman", # PR #19410
2026-05-07 06:29:15 -07:00
"lucianopacheco@gmail.com": "LucianoSP", # PR #19412
2026-05-07 06:30:56 -07:00
"paultian.research@gmail.com": "paul-tian", # PR #19423
"info@glesperance.com": "glesperance", # PR #19443
"lxl694522264@gmail.com": "EvilDrag0n", # PR #20651
# v0.13.0 additions
"clode@clo5de.info": "jackey8616", # via PR salvage
"james.russo@heygen.com": "jrusso1020", # via PR salvage
"leon@sgp43.com": "LeonSGP43", # PR #18739 salvage of #14570
"miniding@miniding.home": "Foolafroos", # PR #20329 French locale
"montbra@gmail.com": "Montbra", # PR #20897 salvage of #16189 (TUI voice PTT)
"275835513+paulb26@users.noreply.github.com": "paulb26", # PR #24135 salvage (pty-bridge killpg)
"promptsiren@gmail.com": "firefly", # PR #18123 salvage of #16660 (ContextVars)
"wtyopenclaw@gmail.com": "WuTianyi123", # PR #20275 salvage of #13723 (feishu markdown)
"zhicheng.han@mathematik.uni-goettingen.de": "hanzckernel", # PR #20311 (api-server approval events)
"agentsmithlaor@gmail.com": "oferlaor", # PR #22356 salvage (cron origin sender identity)
"jhin.lee@unity3d.com": "leehack", # PR #22053 salvage (telegram DM topic reply fallback)
"caojiguang@gmail.com": "caojiguang", # PR #35117 carries #31853 (weixin _api_post/_api_get wait_for)
# pander: empty email, salvaged via PR #19665 from #16126 by @ms-alan
"ayman.a.kamal@hotmail.com": "A-kamal", # PR #18678 (xAI image resolution fix)
# Kanban bug-fix batch salvage (May 2026)
"frowte3k@gmail.com": "Frowtek", # salvage of #23206 (gateway --board auto-subscribe)
"sylw3st3rr@gmail.com": "Sylw3ster", # salvage of #23252 (HERMES_KANBAN_BOARD restore)
"hello@dominikh.com": "dmnkhorvath", # salvage of #23358 (kanban worker send_message)
"413011+smwbev@users.noreply.github.com": "smwbev", # salvage of #23659 (aria-label colLabel)
"58116817+TurgutKural@users.noreply.github.com": "TurgutKural", # salvage of #23356 (HERMES_HOME inject)
"openclaw@agent.local": "29206394", # PR #22194 salvage (sudo -S brute-force guard, #9590)
"freedemon@gmail.com": "fr33d3m0n", # PR #21128 salvage (sudo stdin/askpass DANGEROUS, #17873 cat 4)
"zhaowh3613@outlook.com": "VinceZcrikl", # PR #23647 salvage (npm UTF-8 decode on GBK Windows)
feat(session_search): single-shape tool with discovery, scroll, browse — no LLM (#27590) * feat(session_search): single-shape tool with discovery, scroll, browse — no LLM Replaces the LLM-summarized session_search with a single-shape tool that returns actual messages from the DB. Three calling shapes inferred from args (no mode parameter): 1. Discovery — pass query. FTS5 + anchored ±5 window + bookends per hit, all in one call. ~20ms on a real DB instead of ~90s for the previous three aux-LLM calls. 2. Scroll — pass session_id + around_message_id. Returns a window centered on the anchor. To paginate, re-anchor on the first/last id of the returned window. Boundary message appears in both windows as the orientation marker. ~1ms per scroll call. 3. Browse — no args. Recent sessions chronologically. Bookend_start (first 3 user+assistant msgs) and bookend_end (last 3) give the agent goal + resolution on every discovery hit, so a single tool call reconstructs a long session's arc without loading the whole transcript. The aux-LLM summary path is gone: it cost ~$0.30/call, took ~30s, and laundered FTS5 hits through a model that could confabulate when the right session wasn't in the hit list. The merged shape returns byte-for-byte content from SQLite. History: - PR #20238 (JabberELF) seeded the fast/summary dual-mode split. - PR #26419 (yoniebans) expanded to fast/guided/summary with bookends, multi-anchor drill-down, default-mode config, and a teaching skill. This PR collapses that toolkit into one shape with explicit scroll support, drops the summary path, drops the mode parameter, drops the config knob, drops the skill. JabberELF's seed work is acknowledged via the AUTHOR_MAP entry. Validation: - 38/38 tool tests pass (tests/tools/test_session_search.py) - 12/12 get_messages_around tests pass (tests/hermes_state/) - 11/11 get_anchored_view tests pass (tests/hermes_state/) - Full tests/tools/ run: 5168 passing, 2 failures pre-exist on main (test ordering in test_delegate.py, unrelated) - E2E against live state DB: discovery 20ms, scroll 1ms, browse 280ms; pagination forward+backward works with boundary-message orientation; error paths return clean tool_error responses Co-authored-by: JabberELF <abcdjmm970703@gmail.com> Co-authored-by: yoniebans <jonny@nousresearch.com> * chore(session_search): prune dead LLM-summary config and docs Companion to the single-shape rewrite. The auxiliary.session_search config block, max_concurrency / extra_body tunables, and matching docs sections all referenced the removed LLM summarization path. Removing them so users don't try to tune knobs that nothing reads. - hermes_cli/config.py: drop dead auxiliary.session_search block from DEFAULT_CONFIG. Leftover keys in user config.yaml are harmless and ignored. - hermes_cli/tips.py: drop two tips referencing the removed max_concurrency / extra_body knobs. - website/docs/user-guide/configuration.md: drop 'Session Search Tuning' section and the auxiliary.session_search block from the example. - website/docs/user-guide/features/fallback-providers.md: drop session_search rows from the auxiliary-tasks tables and the dedicated tuning subsection. - website/docs/reference/tools-reference.md: rewrite the session_search entry to describe the new three-shape behaviour. - CONTRIBUTING.md: update the file-tree description. - tests/tools/test_llm_content_none_guard.py: remove TestSessionSearchContentNone class and test_session_search_tool_guarded — both guard against an unguarded .content.strip() call site in _summarize_session() that no longer exists. Validation: 97/97 targeted tests still pass (hermes_state + session_search + llm_content_none_guard). Config tests 55/55. --------- Co-authored-by: JabberELF <abcdjmm970703@gmail.com> Co-authored-by: yoniebans <jonny@nousresearch.com>
2026-05-17 23:28:45 -07:00
"abcdjmm970703@gmail.com": "JabberELF", # PR #20238 seed (session_search dual-mode, evolved into single-shape)
"anton.kuenzi@gmail.com": "ZeterMordio", # PR #11754 salvage (zsh completion compdef + _arguments syntax)
"23yntong@stu.edu.cn": "iuyup", # PR #6155 salvage (shell=True hardening)
"86501179+1RB@users.noreply.github.com": "1RB", # PR #25462 salvage (discord forwarded messages)
"44045943+ayushere@users.noreply.github.com": "ayushere", # PR #25342 salvage (memory teardown leak)
"15791290+domtriola@users.noreply.github.com": "domtriola", # PR #25424 salvage (docs tirith link)
"tuancookiez@gmail.com": "tuancookiez-hub", # PR #34865 salvage (LSP Windows .cmd shim spawn, #34864)
"284216128+ephron-ren@users.noreply.github.com": "ephron-ren", # PR #25358 salvage (MiMo reasoning echo-back)
"96843562+freqyfreqy@users.noreply.github.com": "freqyfreqy", # PR #25423 salvage (docs LSP worktree -> repo)
"54306477+fu576@users.noreply.github.com": "fu576", # PR #25369 salvage (api_mode not inherited cross-provider)
"258095375+kfa-ai@users.noreply.github.com": "kfa-ai", # PR #25398 salvage (whatsapp quoted reply metadata)
"99181308+magic524@users.noreply.github.com": "magic524", # PR #25361 salvage (QQBot reconnect loop)
"9150277+PaTTeeL@users.noreply.github.com": "PaTTeeL", # PR #25359 salvage (custom_providers in compression length)
"1700913+pearjelly@users.noreply.github.com": "pearjelly", # PR #25388 salvage (feishu ws connect override sync)
"100820567+raymaylee@users.noreply.github.com": "raymaylee", # PR #25394 salvage (context compaction status)
"122434621+Tianyu199509@users.noreply.github.com": "Tianyu199509", # PR #25421 salvage (gateway PID Windows)
"58224596+HxT9@users.noreply.github.com": "HxT9", # PR #25760 salvage (web sync-assets cross-platform)
"120411712+evgyur@users.noreply.github.com": "evgyur", # PR #25651 salvage (docs media session context)
"36507055+AsoTora@users.noreply.github.com": "AsoTora", # PR #25624 salvage (MCP auth no-retry)
"98992931+oxngon@users.noreply.github.com": "oxngon", # PR #25603 salvage (forward image attachments to bg tasks)
"37467487+yifengingit@users.noreply.github.com": "yifengingit", # PR #25589 salvage (AUTOINCREMENT id ordering)
"89525629+vanthinh6886@users.noreply.github.com": "vanthinh6886", # PR #25562 salvage (.env 0600 perms)
"16034932+Arkmusn@users.noreply.github.com": "Arkmusn", # PR #25559 salvage (approvals.timeout from config)
"nidhi2894@gmail.com": "nidhi-singh02", # PR #2752 salvage (slack whitespace-only IndexError guard)
"38173192+nidhi-singh02@users.noreply.github.com": "nidhi-singh02",
"Jaaneek@users.noreply.github.com": "Jaaneek", # PR #26457 (xAI Grok OAuth provider)
# v0.14.0 additions
"chuang.guo@hopechart.com": "wuwuzhijing", # PR #21063 salvage (gateway docs mention Weixin)
"nightcityblade@gmail.com": "nightcityblade", # PR #24138 (docs voice/tts table)
"pol.kuijken@gmail.com": "polkn", # PR #6136 salvage (skill_view collision refusal)
"robin@soal.org": "rewbs",
# batch salvage (May 2026 LHF run)
"sauravsejal40@gmail.com": "Saurav0989", # PR #27071 (docs: hermes-eval community link)
"220110965+Saurav0989@users.noreply.github.com": "Saurav0989",
"aviarchi1994@gmail.com": "avifenesh", # PR #25902 (docs: computer-use-linux MCP)
"55848801+avifenesh@users.noreply.github.com": "avifenesh",
"279959838+BROCCOLO1D@users.noreply.github.com": "BROCCOLO1D", # PR #26796 (docs: spotify + HA)
"m@matthewlai.ca": "matthewlai", # PR #25293 (feat: gemma 4 reasoning allowlist)
"4296245+matthewlai@users.noreply.github.com": "matthewlai",
"109617724+0xchainer@users.noreply.github.com": "0xchainer", # PR #27154/27138/27147 salvage
"201800237+kronexoi@users.noreply.github.com": "kronexoi", # PR #27167 salvage (Teams port fallback)
"283442588+EloquentBrush0x@users.noreply.github.com": "EloquentBrush0x", # PR #26642 salvage (post_setup parity)
# batch salvage (May 2026 LHF run, group 2)
"shellybotmoyer@example.com": "shellybotmoyer", # PR #26661 (kanban --severity >=)
"coulson@shellybotmoyer.com": "shellybotmoyer", # PR #25576 (credential_pool ISO rehydrate)
"258858106+shellybotmoyer@users.noreply.github.com": "shellybotmoyer",
"33156212+ether-btc@users.noreply.github.com": "ether-btc", # PR #26632 (memory provider whitespace guard)
"Bloomtonjovish@gmail.com": "LifeJiggy", # PR #26516 (paste collapse logging)
"141562589+LifeJiggy@users.noreply.github.com": "LifeJiggy",
"192385615+LifeJiggy@users.noreply.github.com": "LifeJiggy", # stale salvage commit alias (PR #28315)
"beastant1@gmail.com": "nekwo", # PR #26481 (PS5.1 UTF-8 BOM)
"43717185+nekwo@users.noreply.github.com": "nekwo",
"9785479+stepanov1975@users.noreply.github.com": "stepanov1975", # PR #22074 (setup config picker writes)
"67979730+flooryyyy@users.noreply.github.com": "flooryyyy", # PR #26374 (tool_trace error detection)
"188585318+dgians@users.noreply.github.com": "dgians", # PR #26034 (.ts/.py/.sh docs types)
"zealy@tz.co": "dgians", # PR #26034 (bot-committed by zealy-tzco under dgians' PR)
"mottei.survive@gmail.com": "flanny7", # PR #27030 (setup_open_webui python var)
"20530505+flanny7@users.noreply.github.com": "flanny7",
"hermesagent26@gmail.com": "hermesagent26", # PR #26438 (kimi model-name reasoning pad)
"276067471+hermesagent26@users.noreply.github.com": "hermesagent26",
"71590782+kriscolab@users.noreply.github.com": "kriscolab", # PR #26926 (deepseek default_aux_model)
# batch salvage (May 2026 LHF run, group 3)
"darvsum@users.noreply.github.com": "darvsum", # PR #26766 (preserve discover_models in normalize)
"peter@Peters-Mac-mini.local": "hueilau", # PR #26498 (strip image parts for non-vision)
"33933019+hueilau@users.noreply.github.com": "hueilau",
"32297275+Timur00Kh@users.noreply.github.com": "Timur00Kh", # PR #27114 (telegram DM topic for synthetic events)
"al.bellemare@gmail.com": "Grogger", # PR #27061 (windows console flash suppress)
"7065068+Grogger@users.noreply.github.com": "Grogger",
"18091625+Grogger@users.noreply.github.com": "Grogger", # stale salvage commit alias (PR #28330)
"clement@nousresearch.com": "lemassykoi", # PR #27042 (model-switch probe keyless providers)
"16377344+lemassykoi@users.noreply.github.com": "lemassykoi",
"draplater@icloud.com": "draplater", # PR #26707 (goal judge current time)
"6349758+draplater@users.noreply.github.com": "draplater",
"pr7426@users.noreply.github.com": "pr7426", # PR #27048 (cron parallel job loss)
"rahulnilvan43@gmail.com": "therahul-yo", # PR #26215 (mock keychain in tests)
"kingsleyemeka117@gmail.com": "flamiinngo", # PR #27205 (UnicodeEncodeError footgun checker)
# batch salvage (May 2026 LHF run, group 4)
"283442588+EloquentBrush0x@users.noreply.github.com": "EloquentBrush0x", # PR #26657 (trust_env aiohttp)
"205509009+subtract0@users.noreply.github.com": "subtract0", # PR #25658 (zsh $status -> $rc)
"patryk@jarmakowicz.me": "zwolniony", # PR #26961 (gemini x-goog-api-key)
"12735938+zwolniony@users.noreply.github.com": "zwolniony",
"ambuj@dodopayments.com": "that-ambuj", # PR #26582 (preserve underscores)
"zccyman@163.com": "zccyman", # PR #25294 (custom provider api_key_env alias)
# xAI cluster batch salvage (May 2026)
"lgndscntn@gmail.com": "Fewmanism", # PR #27420 (threaded xAI OAuth callback)
"slimydog@Faisals-Mac-mini.local": "Slimydog21", # PR #28021 (strip slash enums xAI Responses)
"194121339+Slimydog21@users.noreply.github.com": "Slimydog21", # PR #28021 salvage (noreply form)
"bitkyc08@gmail.com": "lidge-jun", # PR #26814 (api server browser security headers)
"sp_ps@Mac-mini.lan": "phoenixshen", # PR #26768 (respect user-configured vision model)
"1594534+phoenixshen@users.noreply.github.com": "phoenixshen",
"147827411+AhmetArif0@users.noreply.github.com": "AhmetArif0", # PR #26635 (line proxy env vars)
# batch salvage (May 2026 LHF run, group 5)
"hari@Hariharans-MacBook-Air-8.local": "haran2001", # PR #27070 (i18n catalog test)
"hariharan15151@gmail.com": "haran2001", # PR #27068 (qwen3.6-plus 1M context)
"56040092+haran2001@users.noreply.github.com": "haran2001",
"1472110+ms-alan@users.noreply.github.com": "ms-alan", # PR #26443 (reload-skills tab completion)
"ganlinbupt@gmail.com": "godlin-gh", # PR #26118 (ACP polished tools)
"wesley.simplicio.ext@siemens-energy.com": "wesleysimplicio", # PR #25777 (xterm.js native selection)
"6108320+wesleysimplicio@users.noreply.github.com": "wesleysimplicio",
"carryzuo00@gmail.com": "Carry00", # PR #26851 (doctor SSH env vars)
"alaamohanad169-ship-it@users.noreply.github.com": "alaamohanad169-ship-it", # PR #26036 (telegram typing after send)
"vigo@hermes": "hawknewton", # PR #26294 (bedrock boto3 lazy_deps)
"211668+hawknewton@users.noreply.github.com": "hawknewton",
"quenvix00@gmail.com": "QuenVix", # PR #26761/26772 salvage
"164776164+QuenVix@users.noreply.github.com": "QuenVix",
"262945885+Mind-Dragon@users.noreply.github.com": "Mind-Dragon", # PR #26966 salvage
"soynchuux@gmail.com": "soynchux", # PR #27060 salvage
"209694554+soynchux@users.noreply.github.com": "soynchux",
# batch salvage (May 2026 LHF run, group 6 — final)
"6666242+bird@users.noreply.github.com": "bird", # PR #25219 (gateway docker exit-75 restart)
"david@loadmagic.ai": "davidcampbelldc", # PR #26834 (web_server proxy_headers=False)
"165905879+davidcampbelldc@users.noreply.github.com": "davidcampbelldc",
"hoangv.pham0803@gmail.com": "hehehe0803", # PR #26212 salvage (codex kanban writable root)
"26063003+hehehe0803@users.noreply.github.com": "hehehe0803",
fix(codex): size and propagate timeouts for Responses-API requests; lower stale defaults Codex / Responses-API requests had three latent timeout bugs that combined into the long silent hangs reported on #21444: 1. The non-stream stale-call detector estimated context tokens from ``api_kwargs["messages"]`` only. Codex / Responses-API payloads carry their conversational load in ``input`` (with ``instructions`` and ``tools``), so every Codex turn logged ``context=~0 tokens`` and the detector never applied its >50k / >100k tier bumps. 2. ``providers.<id>.request_timeout_seconds`` was silently dropped on the main Codex path. The chat_completions path and the auxiliary Codex adapter both forwarded it; the main path skipped it through three places (``build_api_kwargs``, ``ResponsesApiTransport.build_kwargs``, ``_preflight_codex_api_kwargs``). 3. The streaming stale detector had the same payload-shape bug for ``codex_responses`` requests, which route through the non-streaming detector (it's the path that emits the user-facing "No response from provider for 300s (non-streaming, ...)" warning that reporters keep pasting). This commit: - Adds ``estimate_request_context_tokens`` in ``chat_completion_helpers``, used by both the non-stream and stream detectors. Handles ``messages`` (Chat Completions), ``input + instructions + tools`` (Responses API), bare lists, and an unknown-dict fallback. - Forwards ``timeout`` through ``ResponsesApiTransport.build_kwargs`` and ``_preflight_codex_api_kwargs`` (with guards against zero/negative/inf/bool values), and wires ``_resolved_api_call_timeout()`` into the Codex branch of ``build_api_kwargs``. - Lowers the implicit non-stream stale defaults so fallback providers kick in faster when upstream stalls: * base 300s -> 90s * >50k 450s -> 150s * >100k 600s -> 240s These only apply when the user has *not* set ``providers.<id>.stale_timeout_seconds`` or ``HERMES_API_CALL_STALE_TIMEOUT``. Explicit config still wins. - Adds regression tests for the estimator shapes, the new defaults, the context-tier scaling, transport timeout pass-through, and preflight timeout pass-through / rejection of invalid values. Closes #21444 Supersedes #21652 #24126 #31855 Co-authored-by: Hoang V. Pham <26063003+hehehe0803@users.noreply.github.com>
2026-05-25 01:36:22 -07:00
"kasunvinod@users.noreply.github.com": "kasunvinod", # PR #24126 salvage (codex timeout propagation)
"15059870+kasunvinod@users.noreply.github.com": "kasunvinod",
"38348871+vaddisrinivas@users.noreply.github.com": "vaddisrinivas", # PR #26394 salvage (Docker messaging extra)
# batch salvage (May 2026 LHF run, group 7)
"198679067+02356abc@users.noreply.github.com": "02356abc", # PR #28286 salvage (wecom CLOSING)
"1743117+burjorjee@users.noreply.github.com": "burjorjee", # PR #28201 salvage (inline-shell timeout guard)
"keki@MacBookPro.attlocal.net": "burjorjee",
"264690993+oseftg@users.noreply.github.com": "oseftg", # PR #28168 salvage (natural ending emoji/caret)
"hex.hermes@agentmail.to": "oseftg",
"236912655+rudi193-cmd@users.noreply.github.com": "rudi193-cmd", # PR #28241 salvage (empty credential pool)
"rudi193@gmail.com": "rudi193-cmd",
"86684667+sadiksaifi@users.noreply.github.com": "sadiksaifi", # PR #27982 salvage (kanban horiz scroll)
"mail@sadiksaifi.dev": "sadiksaifi",
"231588442+vynxevainglory-ai@users.noreply.github.com": "vynxevainglory-ai", # PR #29233 salvage (kanban scrollbar + body overflow)
"vynxevainglory@gmail.com": "vynxevainglory-ai",
# batch salvage (May 2026 LHF run, group 8)
"266824395+AceWattGit@users.noreply.github.com": "AceWattGit", # PR #28159 salvage (_pool_may_recover NameError)
"57024493+YuanHanzhong@users.noreply.github.com": "YuanHanzhong", # PR #28032 salvage (x.com status link-like)
"24368158+colin-chang@users.noreply.github.com": "colin-chang", # PR #28245/#28249/#28251 salvage
"zhangcheng5468@gmail.com": "colin-chang",
"172729123+felix-windsor@users.noreply.github.com": "felix-windsor", # PR #28019 salvage (cron asterisks)
"felixwindsor3344@gmail.com": "felix-windsor",
"259054917+houenyang-momo@users.noreply.github.com": "houenyang-momo", # PR #28205 salvage (charizard contrast)
"33547839+sir-ad@users.noreply.github.com": "sir-ad", # PR #31941 salvage (compaction noise)
"adarsh.agrahari26@gmail.com": "sir-ad",
"269599864+rdasilva1016-ui@users.noreply.github.com": "rdasilva1016-ui", # PR #31098 salvage (Telegram /start ping)
"rdasilva1016-ui@users.noreply.github.com": "rdasilva1016-ui",
"35931201+iqdoctor@users.noreply.github.com": "iqdoctor", # PR #28095 salvage (windows installer docs)
"29513231+joe102084@users.noreply.github.com": "joe102084", # PR #28151 salvage (whitespace cron responses)
"joe102084@gmail.com": "joe102084",
"4139778+jvinals@users.noreply.github.com": "jvinals", # PR #27936 salvage (Slack U-IDs)
"3001335+maxmilian@users.noreply.github.com": "maxmilian", # PR #28267 salvage (Change Model portal)
"maxmilian@gmail.com": "maxmilian",
"41468846+samggggflynn@users.noreply.github.com": "samggggflynn", # PR #27952 salvage (dingtalk pre_start)
"abc401011721@gmail.com": "samggggflynn",
"yannsunn@users.noreply.github.com": "yannsunn", # PR #28064 salvage (xai proxy upstream)
"yannsunn1116@gmail.com": "yannsunn",
"asdlem@users.noreply.github.com": "asdlem", # PR #27852 salvage (clarify full text in body)
# batch salvage (May 2026 LHF run, group 9)
"1779909+jdelmerico@users.noreply.github.com": "jdelmerico", # PR #28278 salvage (signal require_mention)
"20639347+justemu@users.noreply.github.com": "justemu", # PR #27996 salvage (matrix thread_require_mention)
"justemu@users.noreply.github.com": "justemu",
"57024493+YuanHanzhong@users.noreply.github.com": "YuanHanzhong", # PR #28029 salvage (dashboard scrollback)
"YuanHanzhong@users.noreply.github.com": "YuanHanzhong",
"1663402+noctilust@users.noreply.github.com": "noctilust", # PR #28080 salvage (stale TUI resume env)
"1663402+freeurmind@users.noreply.github.com": "noctilust",
"35164907+MoonJuhan@users.noreply.github.com": "MoonJuhan", # PR #28288 salvage (unreadable JSONL transcripts)
"codemike@naver.com": "MoonJuhan",
"201563152+outsourc-e@users.noreply.github.com": "outsourc-e", # PR #28164 salvage (cron emoji ZWJ)
"201803425+Zyrixtrex@users.noreply.github.com": "Zyrixtrex", # PR #28275 salvage (Google OAuth timeout)
"zyrixtrex@gmail.com": "Zyrixtrex",
"120500656+ooovenenoso@users.noreply.github.com": "ooovenenoso", # PR #28256 salvage (tool loop recovery hints)
"120500656+oooindefatigable@users.noreply.github.com": "ooovenenoso",
"vanthinh6886@gmail.com": "vanthinh6886", # PR #28018 salvage (yaml/flock/atomic write guards)
"erik.engervall@gmail.com": "erikengervall", # PR #28774 (firecrawl integration tag)
"egilewski@egilewski.com": "egilewski", # PR #30432 (MEDIA path traversal fix, GHSA-jmf9-9729-7pp8)
"edison@mcclean.codes": "McClean-Edison", # PR #29817 (register_auxiliary_task plugin API)
"zhangsamuel12@gmail.com": "SamuelZ12", # PR #7480 (show recap after in-session resume)
"490408354@qq.com": "daizhonggeng", # PR #9020 (numbered /resume selection)
"claw@openclaw.ai": "wanwan2qq", # PR #10215 (strip brackets/quotes from /resume; gateway session-ID lookup)
"simo.kiihamaki@gmail.com": "SimoKiihamaki", # PR #30773 (Windows /reset+/new freeze; stdin fallback for modal)
"66773372+Tranquil-Flow@users.noreply.github.com": "Tranquil-Flow", # PR #27518 (bracketed-paste timeout)
"8bit64k@pm.me": "8bit64k", # PR #14681 (TUI /q alias from quit to queue)
"chenglunhu@gmail.com": "hclsys", # PR #31985 (TUI /q alias regression test)
"dearmayo@localhost": "ffr31mr", # PR #32103 (SubdirectoryHintTracker workspace boundary)
"TheOnlyMika@users.noreply.github.com": "TheOnlyMika", # PR #32155 (dashboard XSS + defusedxml)
"krislidimo@gmail.com": "krislidimo", # PR #29775 (tighten Telegram table row-group spacing; drop redundant first bullet)
"timothy.b.dixon@gmail.com": "Codename-11", # PR #29302 (API server session controls — sessions/chat/fork/stream)
"jpschwartz2@uwalumni.com": "Schwartz10", # PR #29302 sub-PR (multimodal media in session chat API)
"JohnC1009@users.noreply.github.com": "JohnC1009", # PR #32020 salvage (auth: global auth.json fallback in _load_provider_state)
"biser@bisko.be": "bisko", # PR #33784 salvage (re-pad reasoning_content on cross-provider fallback to require-side providers)
# v0.15.0 additions
"glen@workmanfirearms.com": "sgtworkman",
"jorge.fuenmayort@gmail.com": "jfuenmayor",
"josh.dow@prepad.io": "joshuadow", # PR #43004 salvage (desktop WS session rebind)
"mordred@inaugust.com": "emonty",
"rodrigoeq@hotmail.com": "rodrigoeqnit",
"soliva.johnpaul@icloud.com": "jonpol01",
"2182712990@qq.com": "yu-xin-c", # PR #32122 (Docker audio bridge notes)
"baxter@bitreserve.ai": "BaxBit", # PR #30200 (Svix webhook signature validation)
"chris.eth@qq.com": "duyua9", # PR #10949 (render object config values structurally)
"ethie@nous": "ethernet8023", # PR #29342 (TUI clipboard copy on linux/wayland)
"jiahuigu@sjtu.edu.cn": "Jiahui-Gu", # PR #29276 (guard pickle.loads in darwinian-evolver)
"justinccdev@gmail.com": "justincc", # PR #28914 (set tool_name on tool-result messages)
"kdkcfp@gmail.com": "slowtokki0409", # PR #29025 (ignore local Hermes runtime files)
"peter.yuqin@gmail.com": "WuKongAI-CMU", # PR #10082 (reject symlinked audio inputs)
"sunil.nitie@gmail.com": "Sunil123135", # PR #31031 (Windows Docker Desktop compose)
"weichangyuwcy@gmail.com": "ChyuWei", # PR #30987 (TUI TTS env var on voice off)
# batch salvage PR #35758 (perf micro-fixes)
"116212274+amathxbt@users.noreply.github.com": "amathxbt", # PR #22155 (cache tool_output_limits)
"takis312@hotmail.com": "ErnestHysa", # PRs #32636/#32708 (MCP asyncio.sleep + O(n^2) watcher drain)
"me@simontaggart.com": "SiTaggart", # PR #35583 (docker_forward_env empty-secret .env fallback)
"2663402852@qq.com": "x1am1", # PR #35098 (chown root-owned top-level HERMES_HOME state files)
"nicsequenzy@gmail.com": "polnikale", # PR #35717 (discover Playwright headless_shell browser)
"wasdhkzk@gmail.com": "whyhkzk", # PR #32407 (sandbox-mirror inner-container guard; commits authored as whyhkzk + zhukun)
"leonard@sellem.me": "leonardsellem", # PR #37405 (desktop WS origin guard on remote/Tailscale binds)
2026-06-06 01:04:25 +05:30
"42903577+ohMyJason@users.noreply.github.com": "ohMyJason", # PR #29810 (discover_models in custom_providers section 4)
"singhsanidhya741@gmail.com": "sanidhyasin", # PR #40403 salvage (model.default_headers for custom OpenAI-compatible providers, #40033)
feat(windows): enable dashboard /chat tab via ConPTY (win_pty_bridge) + tests (#42251) * feat(windows): enable dashboard chat tab via ConPTY (win_pty_bridge) Add hermes_cli/win_pty_bridge.py — a pywinpty-backed drop-in for PtyBridge with the same spawn/read/write/resize/close surface — and wire it into the web_server PTY import block so Windows picks it up instead of falling back to None. pywinpty is already a declared win32 dependency (pyproject.toml). The ConPTY read path runs inside run_in_executor so the event loop is never blocked. Spawn/read/write/terminate call shapes are taken directly from tools/process_registry.py which already exercises the same pywinpty version. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * docs: remove WSL2-only caveat for dashboard chat tab The chat pane now works on native Windows via the ConPTY bridge added in the previous commit. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * test(windows): cover ConPTY bridge + web_server platform-branched import Companion to the bridge added in the previous commits. Verified live on native Windows 11 (pywinpty 2.0.15) against `hermes dashboard`'s `/api/pty` WebSocket: the spawned `hermes --tui` (node entry.js) renders through ConPTY, resize escapes reach `setwinsize`, and closing the WS reaps both the node child and the pywinpty agent with zero orphans. tests/hermes_cli/test_win_pty_bridge.py Mirrors the layout of the existing POSIX test_pty_bridge.py: spawn/io/resize/close/env coverage against cmd.exe and python -c, plus the cross-platform fallback surface (PtyUnavailableError, the off-Windows `spawn -> raises PtyUnavailableError` guard, and the load-bearing _clamp() helper that protects setwinsize from garbage winsize values out of xterm.js). tests/hermes_cli/test_web_server_pty_import.py Asserts that web_server.PtyBridge resolves to WinPtyBridge on win32 and to the POSIX PtyBridge on POSIX, that PtyUnavailableError is the matching class on each side (so isinstance checks in /api/pty's spawn fallback path work), and a source-text check that pins the platform-branched import shape so a future refactor can't quietly collapse it back to a POSIX-only import. scripts/release.py AUTHOR_MAP entries so CI release-note generation can resolve both authors' plain (non-noreply) emails to their GitHub logins. Co-Authored-By: JoelJJohnson <josephjohnson.joel@gmail.com> Co-Authored-By: Nea74 <andreas@schwarz-ketsch.de> --------- Co-authored-by: JoelJJohnson <josephjohnson.joel@gmail.com> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: Nea74 <andreas@schwarz-ketsch.de>
2026-06-08 11:32:43 -07:00
"josephjohnson.joel@gmail.com": "JoelJJohnson", # PR #39913 salvage (Windows ConPTY dashboard chat bridge)
"andreas@schwarz-ketsch.de": "Nea74", # PR #40022 co-author credit (same Windows ConPTY bridge design)
"chanhokyim@gmail.com": "joel611", # PR #33958 salvage (DISCORD_ALLOWED_ROLES role_authorized gateway flag)
}
def git(*args, cwd=None):
"""Run a git command and return stdout."""
result = subprocess.run(
["git"] + list(args),
capture_output=True, text=True,
cwd=cwd or str(REPO_ROOT),
)
if result.returncode != 0:
print(f"git {' '.join(args)} failed: {result.stderr}", file=sys.stderr)
return ""
return result.stdout.strip()
def git_result(*args, cwd=None):
"""Run a git command and return the full CompletedProcess."""
return subprocess.run(
["git"] + list(args),
capture_output=True,
text=True,
cwd=cwd or str(REPO_ROOT),
)
def get_last_tag():
"""Get the most recent CalVer tag."""
tags = git("tag", "--list", "v20*", "--sort=-v:refname")
if tags:
return tags.split("\n")[0]
return None
def next_available_tag(base_tag: str) -> tuple[str, str]:
"""Return a tag/calver pair, suffixing same-day releases when needed."""
if not git("tag", "--list", base_tag):
return base_tag, base_tag.removeprefix("v")
suffix = 2
while git("tag", "--list", f"{base_tag}.{suffix}"):
suffix += 1
tag_name = f"{base_tag}.{suffix}"
return tag_name, tag_name.removeprefix("v")
def get_current_version():
"""Read current semver from __init__.py."""
content = VERSION_FILE.read_text()
match = re.search(r'__version__\s*=\s*"([^"]+)"', content)
return match.group(1) if match else "0.0.0"
def bump_version(current: str, part: str) -> str:
"""Bump a semver version string."""
parts = current.split(".")
if len(parts) != 3:
parts = ["0", "0", "0"]
major, minor, patch = int(parts[0]), int(parts[1]), int(parts[2])
if part == "major":
major += 1
minor = 0
patch = 0
elif part == "minor":
minor += 1
patch = 0
elif part == "patch":
patch += 1
else:
raise ValueError(f"Unknown bump part: {part}")
return f"{major}.{minor}.{patch}"
def update_version_files(semver: str, calver_date: str):
"""Update version strings in source files."""
# Update __init__.py
content = VERSION_FILE.read_text()
content = re.sub(
r'__version__\s*=\s*"[^"]+"',
f'__version__ = "{semver}"',
content,
)
content = re.sub(
r'__release_date__\s*=\s*"[^"]+"',
f'__release_date__ = "{calver_date}"',
content,
)
VERSION_FILE.write_text(content)
# Update pyproject.toml
pyproject = PYPROJECT_FILE.read_text()
pyproject = re.sub(
r'^version\s*=\s*"[^"]+"',
f'version = "{semver}"',
pyproject,
flags=re.MULTILINE,
)
PYPROJECT_FILE.write_text(pyproject)
Add Hermes desktop app (#20059) * feat: better composer etc * docs: add desktop and dashboard run instructions * fix(desktop): address security scan findings * fix(dashboard): resolve @nous-research/ui path under npm workspaces The sync-assets prebuild step shelled out to 'cp -r node_modules/@nous-research/ui/dist/fonts ...' with a path relative to apps/dashboard/. That works only when the dep is installed locally in the dashboard workspace, but 'npm install' at the repo root (the documented setup — see apps/desktop/README.md) hoists shared deps to the root node_modules under npm workspaces. The relative cp then fails with 'No such file or directory', sync-assets exits 1, the Vite build aborts, and 'hermes dashboard' surfaces a generic 'Web UI build failed' message. Replace the shell one-liner with scripts/sync-assets.cjs, which walks up from the dashboard directory looking for node_modules/ @nous-research/ui — working in both the hoisted (workspaces) and co-located (standalone) layouts. Also guards against a missing dist/fonts or dist/assets with a clearer error pointing at a rebuild of the UI package rather than silently copying nothing. * feat(desktop): support connecting to a remote Hermes backend Add HERMES_DESKTOP_REMOTE_URL and HERMES_DESKTOP_REMOTE_TOKEN env vars that, when set, short-circuit the local-child spawn in startHermes() and connect the Electron renderer to an already- running 'hermes dashboard' server reachable over the network. Motivating use case: WSL2 users who want to run the Hermes core (agent loop, tools, filesystem access) inside their WSL distribution while rendering the Electron GUI on native Windows. Before this change, the desktop app always spawned a local Python child on the same host as the renderer, which doesn't cross the WSL/Windows boundary. The remote path reuses waitForHermes() as a liveness probe (/api/status is in the backend's public endpoint allowlist), so the connection is only returned once the backend is actually ready. WebSocket URL derivation picks ws:// or wss:// based on the input scheme. URL validation rejects non-http(s) schemes and requires both env vars together to avoid a half-configured connection that would silently fall through to the spawn path. No behaviour change when the env vars are unset — the default local-spawn flow is untouched. Typical usage: # in WSL2 hermes dashboard --tui --no-open --host 0.0.0.0 --port 9119 --insecure # on Windows set HERMES_DESKTOP_REMOTE_URL=http://localhost:9119 set HERMES_DESKTOP_REMOTE_TOKEN=<session token> set HERMES_DESKTOP_IGNORE_EXISTING=1 (launch Hermes desktop) * ci(desktop): automate desktop releases Add GitHub Actions release channels for signed desktop installers and document the stable/nightly download paths. * feat: file tabs * refactor(desktop): tighten right-rail tab close API Promote closeRightRailTab/closeActiveRightRailTab as the single public entry point. Drops the activeTabRef + handleCloseDocument indirection in ChatPreviewRail, the unused $rightRailHasContent atom, and the legacy dismissFilePreviewTarget alias. -70 LOC. * feat(desktop): polish composer pill toward reference look Solid foreground-on-background send/voice-conversation circle (black-on-white in light, white-on-black in dark) anchors the right edge as the primary CTA instead of the orange theme primary. Bumps the primary control to 2.125rem so it visually outranks the ghost mic/plus controls. Opens up the surface padding (0.625rem x / 0.5rem y) so the input row breathes around its controls, and nudges the corner radius from 20 to 24px for a slightly pill-ier silhouette. LiquidGlass distortion is preserved. * feat(desktop): add startup and onboarding flow Add phase-based desktop boot progress, fresh-install sandbox testing, and first-run provider credential onboarding so packaged installs can start cleanly without manual settings detours. * fix(desktop): gate prompts on provider setup Show the desktop provider onboarding flow before prompt submission when no inference provider is configured, preventing fresh installs from falling through to backend credential errors. * fix(desktop): surface provider onboarding from session warnings Propagate credential warnings through session runtime info and open desktop onboarding whenever a session reports no usable provider, so unconfigured installs cannot fall through to prompt errors. * fix(desktop): route gateway provider errors to onboarding The "No inference provider configured" auth error reaches the renderer through gateway error events, not the prompt.submit promise; the previous patch only caught the latter, so the error toast still surfaced and onboarding never opened. Also strip credential-shaped env vars from the test:desktop:fresh sandbox so the packaged backend can't see provider keys leaking from the launching shell. * fix(desktop): use strict runtime check to drive onboarding setup.status returned True whenever any provider auth state was discoverable, including indirect fallbacks like a gh-CLI Copilot token. That made desktop think the user was set up while the agent's actual resolve_runtime_provider call still raised AuthError, leaving the user with a useless toast and no onboarding. Add a setup.runtime_check gateway method that runs the same resolver the agent uses on session creation, and switch the desktop onboarding overlay and prompt precheck to use it. * feat(desktop): OAuth-first onboarding using existing dashboard provider API Replace the engineer-flavored API key form with a Sign-in-first onboarding overlay that uses the dashboard's existing /api/providers/oauth catalog and PKCE/device-code endpoints (Anthropic, Nous, OpenAI Codex, etc.). API key entry is now a fallback tab with friendly provider names instead of env var prefixes, and the loud raw resolver error is gone in favor of a one-line welcome message. * fix(desktop): polish onboarding provider list Reorder OAuth providers so Nous Portal is first, give the segmented Sign in / API key control equal column widths, and replace the engineer-flavored backend names like "Anthropic (Claude API)" / "MiniMax (OAuth)" with friendlier in-app titles. External-CLI providers now show a softer subtitle and an external-link icon instead of a chevron. * refactor(desktop): split onboarding overlay into store + view Move the OAuth state machine, runtime check, copy-to-clipboard, and api-key save into store/onboarding.ts (matching the boot.ts pattern), leaving the overlay as a presentation layer that subscribes via useStore. Tabs are now table-driven, child panels read flow from the store instead of prop-drilling, and the polling/PKCE/error/success branches share a small Status atom. * fix(desktop): external CLI providers + center mode tabs External-CLI providers (Claude Code, Qwen Code) now open an in-overlay panel with the CLI command, copy button, and an "I've signed in" recheck instead of firing an invisible toast. Center the Sign in / API key tab control so it sits under the heading instead of hugging the left edge. * fix(desktop): drop onboarding tabs for an inline link, group device-code waiting state Replace the Sign in / API key tab pair with an "I have an API key" footer link under the OAuth provider list, with a "Back to sign in" affordance inside the API key form. Group the device-code "Waiting for you to authorize..." status next to the Cancel button so the alignment matches the action. * refactor(desktop): tighten onboarding store + overlay Drop the dead isOnboardingBusy/BUSY set, factor the catch-fallback dance into safeReq, and share a single reloadAndConnect helper between PKCE submit, device-code success, external recheck, and api-key save. In the overlay, extract Step / CodeBlock / FlowFooter / CancelBtn / DocsLink atoms so the four sign-in panels share the same chrome instead of repeating it inline. Net effect: fewer literal divs, one place to touch the spacing, and the code-block + footer rows are reusable across future flows. * fix(desktop): mount onboarding from frame 1 to kill the FOUT Default onboarding.configured to null (unknown until the runtime check resolves) and have the onboarding overlay render whenever it's not yet confirmed true. The boot overlay now yields to it, so the very first paint is the Welcome card with a "While we get you set up..." progress strip instead of a flash of the chat shell between boot dismiss and onboarding mount. The picker swaps in cleanly once the gateway opens and the runtime check confirms the user is not configured. Already-configured users see the same prep card briefly while their existing runtime warms up, then the overlay dismisses without touching the chat shell. * fix(desktop): top-align empty sessions placeholder The "Start a chat to build your history." empty state used a min-h-35 grid place-items-center container, which floated the text in a tall dead zone. Render it as a flat paragraph that sits right under the section header like the empty pinned state does. * refactor(desktop): drop dead boot overlay Onboarding overlay subsumes the boot card now that it mounts from frame 1 and renders boot progress inline. The standalone DesktopBootOverlay is unreachable in every flow (yields whenever onboarding has not confirmed configured, dismisses once it has). * fix(desktop): hide pinned/recents sections until first session A fresh sidebar showed the Pinned and Recent chats headers with floating empty-state copy underneath. Drop both sections (and the now-orphan SidebarEmptySessionState) when there are no sessions yet — they reappear after the first chat. Skeletons during initial load are unchanged. * feat(gui): route embedded TUI through dashboard gateway (#21979) Inject HERMES_TUI_GATEWAY_URL into dashboard PTY sessions so embedded ui-tui instances attach to the in-process websocket gateway, with coverage for the new env wiring. * Add desktop remote gateway settings Make the desktop gateway connection configurable from settings so local remains the default while remote backends can be saved, tested, and applied without environment variables. * feat(gui): first-class Messaging page + gateway menu redesign - Add Messaging page to the desktop app with per-platform setup, status, and inline guidance. Catalog derives from gateway.config Platform enum + plugin registry, so every messaging adapter the CLI supports (Telegram, Discord, Slack, Mattermost, Matrix, WhatsApp, Signal, BlueBubbles, Home Assistant, Email, SMS, DingTalk, Feishu, WeCom, Weixin, QQ, Yuanbao, API server, Webhooks, plugins) shows up without per-platform code. - New REST endpoints: GET /api/messaging/platforms, PUT and POST /test on the same path. Secrets go through the existing .env pipeline; enable/disable writes config.yaml. - Replace gateway statusbar dropdown with a richer panel: status row, icon-only restart + system-panel actions, recent activity (with timestamps trimmed in display, full text on hover), platform list. - Auto-poll the messaging page every 6s (paused when hidden) so status updates without a manual check. - Drop Settings / Command Center from the sidebar nav (still reachable via shortcuts and the titlebar cog). - Flatten top corners on Messaging/Skills/Artifacts/Chat panes. - Share new StatusDot component across messaging + gateway menu. - Fix gateway/config.py so an explicit platforms.<name>.enabled=false in config.yaml is honored when env tokens are present. - pb-9 on the chat content area for breathing room above the composer. * Potential fix for pull request finding 'CodeQL / Clear-text logging of sensitive information' Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> * pin electron version * hide application menu on non-mac systems * interpret compactPreview for non-string vlaues as JSON or an empty string * fix(desktop): keep composer contenteditable mounted across stacked toggle The composer rendered {input} inside two different parent fragments depending on `stacked`. When auto-expand flipped `stacked` (e.g. the moment typed text wrapped past two lines), React reconciled the two branches as different positions and unmounted/remounted the contenteditable. The fresh mount started empty, so any in-flight characters — most reliably reproduced by holding a key — were lost. Replace the conditional with a single CSS Grid whose template-areas swap on `stacked`. The three children (menu, input, controls) keep stable identities across the toggle; only their grid placement changes, which the browser handles without React tearing down the editor. * refactor(desktop): align install layout with install.ps1 / install.sh Make the desktop app's runtime layout match what scripts/install.ps1 and scripts/install.sh produce, so a desktop-only user and a CLI-only user end up with the same files in the same places and can share one install. Layout - ACTIVE_HERMES_ROOT = HERMES_HOME/hermes-agent (was: process.resourcesPath/hermes-agent, read-only) - VENV_ROOT = HERMES_HOME/hermes-agent/venv (was: userData/hermes-runtime) - desktop.log = HERMES_HOME/logs/desktop.log (was: userData/desktop.log) - HERMES_HOME default: %LOCALAPPDATA%\hermes on Windows, ~/.hermes elsewhere The packaged .app/.exe still ships a read-only payload at process.resourcesPath/hermes-agent (FACTORY_HERMES_ROOT). On first launch or after an installer-driven upgrade we sync factory -> active, then provision the venv and run pip install -e . against the active root. Key behaviors - Pin HERMES_HOME in the spawned Python's env so get_hermes_home() resolves to the same path resolveHermesHome() picked. Without this, Python falls back to ~/.hermes on every platform - fine on mac/linux, a split-state bug on Windows where our default is %LOCALAPPDATA%\hermes. - Detect developer installs by .git presence at ACTIVE; never overwrite a user's checkout via factory sync. - Marker at ACTIVE/.hermes-desktop-runtime.json (schema v4) tracks pyproject hash + factory version + runtime schema version. depsFresh fast-paths when nothing changed. - Dev (npm run dev) prefers SOURCE_REPO_ROOT over ACTIVE so devs run their local edits, not whatever's under HERMES_HOME. - Better error messages distinguish "no payload" from "no Python". - Preserve a legacy ~/.hermes on Windows when no %LOCALAPPDATA%\hermes exists, so users with prior pip/manual installs aren't orphaned. pyproject.toml - Promote fastapi, uvicorn[standard], ptyprocess (non-Windows), and pywinpty (Windows) to main dependencies. The dashboard backend (hermes dashboard) needs them at runtime; the previous lazy-import fallback was a footgun for fresh installs. - Empty the [pty] optional-extra; kept as a no-op back-compat alias for any existing pip install hermes-agent[pty] invocations. Drops the hardcoded BUNDLED_RUNTIME_REQUIREMENTS list in main.cjs - the desktop now installs whatever pyproject.toml says, single source of truth. Files - apps/desktop/electron/main.cjs: runtime layout, HERMES_HOME pin, factory->active sync, marker v4 - apps/desktop/scripts/test-desktop.mjs: track new venv location - apps/desktop/README.md: new Setup, Runtime Bootstrap, and Debugging sections - pyproject.toml: fastapi/uvicorn/pty backends in main dependencies; [pty] extra emptied Tested locally on Windows: npm run dev boots cleanly, sessions land at the new location, type-check + lint + test:desktop:platforms all pass. Verified end-to-end on a fresh Win11 VM via dist:win installer. Known gaps (filed as follow-ups, not in this PR): - Skills not seeded on packaged installs (sync_skills only runs in cmd_chat, not cmd_dashboard). Need to move to shared pre-dispatch. - Git Bash not bundled or detected; agent's terminal tool errors out with a useful message but desktop bootstrapper should pre-flight it. - install.ps1 / install.sh should be decomposed into composable phase libraries so the desktop bootstrapper can reuse them as a single source of truth across all install surfaces. * feat(desktop): theme polish, prose chat typography, composer chrome - DS tokens/midground, Backdrop, scoped scrollbars, typography plugin + prose - Composer liquid/radius utilities, thread font parity, tool/thinking cues - File tree label scale, preview flex, thread retry loading + streaming tests * feat(desktop): NSIS prereq detection page + auto-install via winget The packaged Windows installer now detects Python 3.11+ and Git for Windows at install time and offers to install missing prereqs via winget. Mirrors the prereq logic scripts/install.ps1 already runs for CLI installs, so desktop installer users get the same out-of-the-box experience as install.ps1 users. Why - Hermes' terminal tool calls bash.exe directly (tools/environments/ local.py); on Windows that's Git Bash from Git for Windows. Without it, the agent fails on the first terminal() call. - Hermes' Python runtime needs 3.11+. Without it, the desktop bootstrapper errors out at venv creation. - Both gaps surfaced on a fresh Windows 11 VM smoke test: VM had Python pre-installed but no Git, so the agent's first terminal call failed with "Git Bash isn't installed." - install.ps1 has had Install-Git + Install-Uv functions for ages. The desktop installer was the asymmetric outlier. How — NSIS prereq page - New file: apps/desktop/installer/prereq-check.nsh (plugged into electron-builder via build.nsis.include) - Real Wizard page using nsDialogs, inserted via customPageAfterChangeDir hook (between the Directory page and InstFiles). - Group boxes for Python and Git, each showing detection status. - Pre-checked install checkboxes when winget is available. - Auto-skips silently if both prereqs are already installed. - Falls back to manual download URLs when winget itself is missing. - Detection: - Python: probes `py -3.11`/`-3.12`/`-3.13`/`-3.14` via the Python launcher. Microsoft Store "Python stub" (no py.exe) is correctly classified as not-installed. - Git: `where git`. - winget: `where winget` (Win10 1809+ / Win11 with App Installer). - Install execution (in customInstall macro): - Python: nsExec::ExecToLog with `--scope user --silent`. Per-user install, no UAC prompt, output streams to install log. - Git: ExecShellWait via Windows ShellExecute. Critical because Git always installs per-machine and triggers UAC; ShellExecute preserves the foreground focus chain across non-elevated → elevated process spawns, so UAC actually comes to the foreground. nsExec::ExecToLog breaks the chain because winget runs hidden. - Both pass `--disable-interactivity --accept-package-agreements --accept-source-agreements` to suppress winget's own dialogs. - Verification: probes Git's standard install locations via FileExists rather than `where git`. NSIS's process inherits PATH at startup, so a freshly-installed Git won't be visible to `where` until restart. - Silent installs (/S) skip the prompts; managed deploys handle prereqs out-of-band via Group Policy / Intune. How — Electron-side safety net - New findGitBash() in main.cjs, parallel to findSystemPython(). Probes the same locations as tools/environments/local.py:_find_bash() so a positive result here means the agent's terminal tool will work. - ensureRuntime now throws a clear, actionable error on Windows when Git Bash isn't found, matching the existing "Python 3.11+ is required" error path. - Catches users the NSIS page doesn't: .msi installer users (NSIS prereq page doesn't run for MSI), `npm run dev` users, manual installers, anyone who unchecked the install boxes on the NSIS prereq page. - All gated on `IS_WINDOWS`; macOS / Linux unaffected. NSIS build issue (resolved) - electron-builder defaults to `-WX` (warnings as errors). NSIS optimizer emits "warning 6010: function not referenced" for our page functions because Page custom directives don't count as references in its static-analysis pass. The functions ARE called at runtime when NSIS invokes the page; the optimizer just can't see it statically. - Set `build.nsis.warningsAsErrors=false` in package.json so this spurious warning doesn't fail the build. (Documented option from electron-builder's nsisOptions.) Out of scope (filed for future work) - MSI prereq detection: Windows Installer custom actions are a different mechanism. Enterprise deploys typically handle prereqs via GP/Intune. - Bundle PortableGit + python-build-standalone in extraResources for zero-network installs. ~80MB increase. - Mac / Linux GUI prereq flows (different installer formats; Xcode CLT covers most macOS prereqs already; Linux is per-distro hard). Files - apps/desktop/installer/prereq-check.nsh (new, ~290 lines NSIS) - apps/desktop/package.json (build.nsis.include + warningsAsErrors) - apps/desktop/electron/main.cjs (findGitBash + preflight) - apps/desktop/README.md (Runtime prerequisites section) Cross-platform impact - macOS / Linux builds (dist:mac, dist:mac:dmg, dist:mac:zip): nsis config is ignored entirely; .nsh is dormant. - npm run dev: .nsh dormant; main.cjs preflight gated on IS_WINDOWS. - scripts/install.ps1, scripts/install.sh: no reference to any new files; CLI install paths untouched. - Hermes CLI / dashboard / gateway: no reference; runtime untouched. - All checks: node --check on main.cjs and test-desktop.mjs pass; npm run test:desktop:platforms 4/4 passing; node --test green. Tested - npm run dist:win produces signed .exe and .msi without errors. - Fresh Win11 VM (Python pre-installed, no Git): prereq page renders, Python check shows detected, Git checkbox pre-checked. Click Next → Git installs via winget with UAC prompt in foreground. - After install completes, Hermes launches and the agent's terminal tool can run bash commands. Verified Git Bash is detected at `C:\Program Files\Git\bin\bash.exe` by ensureRuntime's preflight. * feat: theme changes, composer tweaks, in app update ux, finesse * fix(cli): seed bundled skills on dashboard + gateway entrypoints `sync_skills(quiet=True)` was only being called from inside `cmd_chat`, which meant `hermes dashboard` (the desktop GUI's backend) and `hermes gateway` (Telegram/Discord/Slack/etc daemons) never seeded the bundled skill library into ~/.hermes/skills/. This surfaced as "No skills found" in the desktop GUI's skills panel on fresh installs, despite the agent having access to the full bundled library when invoked via `hermes chat`. scripts/install.ps1 worked around it by running skills_sync.py as part of Copy-ConfigTemplates, but that's not part of the desktop installer's bootstrap chain. Fix - Extract the skills-sync block from cmd_chat into a module-level `_sync_bundled_skills_quietly()` helper. - Call the helper from cmd_chat (preserving existing behavior), cmd_dashboard (after the --status/--stop early-return paths and fastapi import check, so we don't run skills_sync on management commands or when deps aren't installed), and cmd_gateway. Why these three entrypoints - cmd_chat: the user's primary CLI entrypoint - cmd_dashboard: the desktop GUI's backend; this is what `hermes dashboard --tui` invokes when the desktop bootstrapper spawns Hermes - cmd_gateway: long-running daemons where the user expects the agent to have full skill access Other entrypoints (cmd_config, cmd_doctor, cmd_login, cmd_status, etc.) are management commands that don't need skill discovery and were never running skills_sync in the first place — leaving them alone. Idempotence - tools/skills_sync.py is manifest-based: skipped skills cost milliseconds. Calling it from multiple entrypoints adds no real cost, and users running `hermes chat` then `hermes dashboard` get two fast no-ops on the second call. Failure handling - Helper wraps skills_sync in try/except. Skills are an enhancement, not a hard dependency — Hermes runs fine with an empty skills/ dir. Files - hermes_cli/main.py: + new helper `_sync_bundled_skills_quietly()` at module level + cmd_chat: replace inline block with helper call + cmd_dashboard: add helper call after fastapi import succeeds + cmd_gateway: add helper call before delegating to gateway_command * feat(desktop): hoisted todo widget, JSON tool summaries, history grouping & timer fixes - Hoist todo to first-class widget (shadcn checkboxes, brand colors, no tool-accordion). Header derives label from active task; non-active rows fade. - Replace raw JSON dumps with structured key/value summaries via formatToolResultSummary; nested error extraction for clearer failures. - Fix loaded-session grouping: stitch interleaved assistant/tool iterations into one bubble instead of orphaned synthetic messages. - Stable tool/thinking timers via keyed registry so unmount/scroll doesn't reset elapsed counts; gate "running" on real live thread state. - Reorganize chat-only assistant-ui components under components/chat/. * fix(desktop): address CodeQL alerts on PR #20059 - settings/helpers.ts: harden setNested against prototype pollution. POLLUTING_PATH_PARTS check is now applied at every assignment site (loop + leaf) and uses Object.defineProperty so CodeQL can see the guard inline rather than via a helper function call. - lib/markdown-preprocess.ts: rebuild the dangling-fence close regex from a fence-char + length instead of marker.replace(...). The marker is captured by `(`{3,}|~{3,})` so it can only be backticks or tildes, but CodeQL was tracing tainted input text into the RegExp source and flagging hostname dots from input as part of the pattern (false positive js/incomplete-hostname-regexp on the test fixture URLs). Reconstructing from a literal char breaks the dataflow. - scripts/notarize-artifact.cjs: drop args from the run() rejection message. Args carry --key-id / --issuer / key file path; the existing outer catch already squashes errors to a generic line, but CodeQL was flagging the args.join(' ') as clear-text logging of APPLE_API_KEY_ID. Composer DOM-text-as-HTML alerts (composer/index.tsx:379, :547) are already addressed in 4dd9732a9 — innerHTML assignment was replaced with renderComposerContents which builds DOM via replaceChildren / append text nodes (no HTML interpretation). * fix(desktop): inline prototype-pollution guard so CodeQL sees it CodeQL's dataflow doesn't follow the helper-function guard inside `safeSet`, so it kept flagging Object.defineProperty as prototype- polluting. Inline the literal `__proto__`/`constructor`/`prototype` check at the assignment site to break the dataflow. Behavior unchanged — same set of disallowed keys, same throw. * feat(ui-tui): resolve links to readable page titles Mirror desktop pretty-link behavior in the TUI by resolving HTTP links to page titles with shared caching and safe fetch filters, plus slug-based fallbacks so chat links stay readable even when title fetch fails. * fix(desktop): drop RegExp from dangling-fence close detection Previous attempt tried to break the dataflow by reconstructing the close-fence regex from a literal char + marker.length, but CodeQL still traced marker.length back to input and kept flagging the test-fixture URLs as hostname-regex sources (js/incomplete-hostname-regexp). Replace `new RegExp(...)` + `closeRe.test(body)` with a string-only hasCloseFenceLine() helper that splits on '\n' and uses ===. No regex on this path now, so input data can no longer reach a RegExp source. Behavior preserved: matches lines that are (whitespace + marker + whitespace), which is what the original `\n[ \t]*${marker}[ \t]*(?=\n|$)` matched. All 12 markdown-text tests still pass. * fix(process-registry): suppress windows-footgun false positive on guarded killpg Keep the existing POSIX-only process-group teardown path, but make the signal selection explicit via getattr and add an inline windows-footgun suppression marker on the guarded os.killpg line so the Windows footgun check no longer blocks CI on this intentionally platform-gated code. * feat(desktop): reconcile live tool events, polish thread chrome, harden boot - chat-messages: match tool rows by overlapping query/context/preview values so preview-first `tool.progress` rows reliably adopt later stable-id `tool.start` payloads instead of spawning ghost rows or mis-merging parallel same-name calls; preserve prior args/result across phases. - tui_gateway: emit full args + parsed result on `tool.start` / `tool.complete`, drop redundant `tool.started` re-emit from `tool.progress`. - electron/main: prefer SOURCE_REPO_ROOT before PATH `hermes` in dev so local backend edits actually run; split hardening helpers into `electron/hardening.cjs` with tests. - thread/tool UI: one-shot enter animation keyed by stable ids, braille spinner for running rows, Cursor-like disclosure rows, drill-down + duration/count formatting via new tool-fallback-model. - composer: extract `text-utils`, drop liquid-glass overrides. - right-rail: split preview-pane into preview-console / preview-file. - runtime: incremental external-store runtime + runtime-readiness gate; onboarding store + tests; route-resume hook test. - regression tests for live tool reconciliation (parallel tools, id-less progress, preview-first rows, structured args/results). * feat(desktop): add ripgrep to NSIS prereq page + polish layout Add ripgrep as a third (recommended) prereq alongside Python and Git in the NSIS prereq detection page, and clean up the page layout based on on-VM testing. Why ripgrep - Hermes' search_files tool calls `rg` directly for content + filename search (tools/file_operations.py:1382). Falls back to grep/find from Git Bash when missing — works but slower and noisier (no .gitignore awareness). - ~5MB winget install via `BurntSushi.ripgrep.MSVC --scope user` — no UAC prompt, parallel to how Python installs. - scripts/install.ps1 already installs ripgrep as part of Install-SystemPackages; this brings the desktop installer to parity. Why "recommended" not "required" - Python and Git are hard requirements: without them the agent runtime or terminal tool refuses to start. The bootstrapper preflight throws. - ripgrep is a performance enhancement: missing it just means slower searches. Page wording reflects this; failure to install is logged but doesn't show a MessageBox or block. Layout polish (response to on-VM screenshot review) - Wizard header now correctly reads "System Requirements" instead of the leftover "Choose Install Location" from the previous page. Set via `GetDlgItem $HWNDPARENT 1037/1038` + WM_SETTEXT — the standard NSIS pattern for overriding the page header on a custom Page. - Removed redundant in-body title + verbose intro paragraph; the wizard header IS the title now. Body has one short intro line. - Group boxes tightened to 26u with content positioned just below the groupbox title (not top-anchored status + bottom-anchored checkbox with empty space in the middle). All three panels + footer fit comfortably in 126u, well under the 140u page limit. - Checkbox labels simplified: dropped "(per-user, no admin prompt)" and "(administrator approval required)" suffixes. The footer note still calls out UAC for Git when relevant. - Footer text trimmed to fit cleanly without clipping. Install order (in customInstall macro) - Python → ripgrep → Git - Python and ripgrep are silent and run first; Git's UAC prompt comes last so the user's approval interaction isn't interrupted by silent activity afterwards. Skip behavior unchanged - All three detected → page auto-skips via Abort - Silent install (/S) → customInstall winget block skips - User unchecks all → page advances without running winget Files - apps/desktop/installer/prereq-check.nsh: ripgrep detection block, ripgrep page panel + checkbox, ripgrep customInstall block, GetDlgItem header override, layout reflow - apps/desktop/README.md: Runtime prerequisites section updated to list ripgrep as recommended, with manual winget command * feat(desktop): add model-confirmation step to onboarding After OAuth/API-key login completes, onboarding now shows a confirmation card with the curated default model and a Change button before dropping the user into chat. Closes the gap where the desktop's `model.default` was empty after first launch and the agent had to fall back to whatever heuristic happened to fire — leaving users wondering "why am I getting sonnet-4 when I logged into Nous Portal?" Why - Desktop onboarding only persisted credentials, never `model.default`. The CLI's `hermes model` command pairs provider + model selection, but the desktop's onboarding skipped the model step entirely. - Result: users saw whichever model the agent's auto-fallback picked, unpredictably and undocumented. - For the BUILD demo we want users to land on the model they expect for their provider, with a clear "this is what you're getting" UI and a one-click path to change it before chatting. How - New `confirming_model` flow status carries the just-authenticated provider slug, current default model, label, and a saving flag. - `completeWithModelConfirm()` runs after credentials succeed: reloads env, verifies runtime, fetches /api/model/options to find the curated first-model for the provider, persists it via /api/model/set, then transitions into `confirming_model`. - If anything fails (no providers returned, network error), falls through to the previous behaviour — onboarding completes without the confirm step. Polish, not a hard requirement. - All four credential paths (device_code OAuth, PKCE OAuth, external CLI flow, API key) now use completeWithModelConfirm instead of reloadAndConnect. UI - `ConfirmingModelPanel` shows: green "<provider> connected" banner, card with "Default model: <name>" + Change button, and a "Start chatting" CTA that finalises onboarding. - Reuses the existing `ModelPickerDialog` (the same picker available from the chat shell) for the change-model UX. Search, filtering, multi-provider listing — all already built. - Stacking: ModelPickerDialog defaults to z-130, which renders UNDER the onboarding overlay (z-1300) and breaks pointer events. Added optional `contentClassName` prop to ModelPickerDialog so callers can override; onboarding passes `z-[1310]`. Provider-slug matching - For OAuth flows: pass `provider.id` directly as the preferred slug. - For API-key flows: `OPENROUTER_API_KEY` → "openrouter" via env-key prefix strip. Also includes the user-visible label as a fallback candidate. - fetchProviderDefaultModel falls back to the first authenticated provider in the response if no preferred slug matches — so even a miss still surfaces a reasonable default. Files - apps/desktop/src/store/onboarding.ts: + new `confirming_model` flow variant + fetchProviderDefaultModel + completeWithModelConfirm helpers + setOnboardingModel (optimistic update + revert on failure) + confirmOnboardingModel (finalises onboarding from the card) - reloadAndConnect (replaced; the four call sites now go through completeWithModelConfirm) - apps/desktop/src/components/desktop-onboarding-overlay.tsx: + ConfirmingModelPanel component + new branch in FlowPanel for status `confirming_model` + ModelPickerDialog usage with z-[1310] content class - apps/desktop/src/components/model-picker.tsx: + optional `contentClassName` prop on ModelPickerDialog so the dialog can be stacked on top of other fixed overlays Tested - `npm run type-check` passes - `npx eslint` clean on touched files - Live test in `npm run dev`: cleared onboarding cache, walked through Nous device-code flow, saw confirm card with curated default, clicked Change → ModelPickerDialog rendered above the onboarding overlay with working pointer events, picked a different model, "Start chatting" persisted to ~/.hermes/config.yaml. * fix(desktop): suppress generic provider warning in onboarding Hide the red setup notice when the message is the generic missing-provider guidance, since onboarding already presents provider auth actions. Centralize provider-setup matching across desktop hooks and add coverage for the matcher. * fix(desktop): add 2u clearance below prereq checkboxes Group box bottom border was clipping the checkboxes by 1-2px. Bumped each box height 26u→30u; checkboxes now sit 2u above the bottom border. * fix(nix): refresh dashboard lockfile hash Update the web npm deps hash in nix/web.nix to match the committed apps/dashboard/package-lock.json so bb/gui passes the nix lockfile check. * fix(desktop): install TUI deps in release workflow Ensure desktop release builds install the standalone ui-tui package before bundling the TUI payload. * fix(desktop): run release builder from app package Invoke the desktop builder through the package script so electron-builder uses apps/desktop/package.json. * fix(desktop): expand release artifact names safely Build desktop artifact names from workflow version/channel while preserving electron-builder platform macros. * fix(desktop): use package artifact naming in release workflow Let electron-builder's desktop package config provide platform-specific artifact extensions while the workflow injects the release version/channel metadata. * fix(nix): fetch dashboard npm deps from package root Point the dashboard npm dependency fetch at apps/dashboard so Nix can find the package lockfile after the dashboard move. * fix(nix): build dashboard from package directory Set the web package source root to apps/dashboard so npm patch/build phases run beside the dashboard lockfile while keeping apps/shared available as a sibling. * feat(desktop): render LaTeX math via KaTeX after streaming completes Add @streamdown/math plugin to the chat markdown renderer. Inline ($x^2$) and block ($$...$$) math both supported with singleDollarTextMath enabled. Plugin is gated to non-streaming state to match the existing pattern for syntax highlighting — math renders when the message completes, avoiding KaTeX re-render churn during streaming. KaTeX CSS is imported in styles.css; ~30KB CSS + ~430KB JS added to the bundle. Smoothness improvements during streaming deferred to a follow-up. * perf(desktop): memoize KaTeX renders so math streams without re-rendering Wrap rehype-katex with a per-equation LRU cache (keyed by displayMode + source text) and re-enable math during streaming. Stock @streamdown/math runs rehype-katex on every markdown commit, so each new token re-katexes every equation in the message. For math-heavy responses (an equation derived step-by-step) that's hundreds of ms of wasted work per token and the streaming UI chokes. With memoization, each equation pays katex.renderToString exactly once; subsequent tokens re-walk the tree but hit cache for unchanged equations. The wrapper mirrors rehype-katex's semantics exactly: same class detection (language-math, math-inline, math-display), same <pre>-walk-up for fenced math blocks, same parent.children.splice replacement, same SKIP traversal, same strict-then-lenient render strategy with VFile message reporting. Cached children are structuredCloned on each splice so downstream rehype plugins or toJsxRuntime can't mutate the cache. * fix(desktop): declare katex-memo deps directly + drop per-app lockfile katex-memo.ts (added in 112cad59b) imports hast-util-from-html-isomorphic, hast-util-to-text, remark-math, katex, and unist-util-visit-parents but those were never added to apps/desktop/package.json. They were silently resolving via @streamdown/math at the workspace root, which broke the moment `npm i --prefix apps/desktop` ran with the per-workspace lockfile because that install only consults apps/desktop/package.json. Add them as direct deps, plus unified/vfile/@types/hast for the type imports. Also delete apps/desktop/package-lock.json — root package.json declares workspaces: ["apps/*"], so npm manages all lockfile state at the root. The stale per-app lockfile is what made `npm i --prefix apps/desktop` diverge from the workspace install in the first place and left an empty apps/desktop/node_modules/@assistant-ui/ stub that Vite's dep optimizer then tried (and failed) to open at @assistant-ui/core/dist/internal.js. * feat(desktop): disable Backdrop noise overlay by default The noise overlay defaulted to on, which adds a busy speckle layer over the whole window for every new user. Flip the Leva default to off; the toggle stays in Backdrop / Noise for anyone who wants it back. * fix(desktop): polish LaTeX rendering — currency, code blocks, brackets Five distinct bugs surfaced from a math-heavy stress test: 1. Adjacent code fences glued together. scrubBacktickNoise's second-pass regex /``\s*``/g matched the LAST 2 backticks of one fence + whitespace + FIRST 2 backticks of the next, collapsing two blocks into one. Fixed with lookbehind/lookahead so we only match exactly 2 backticks not part of a longer run. 2. Whitespace eaten between fences and following content. stripPreviewTargets internally calls .trim() which strips leading/ trailing whitespace from each split-segment. For segments between two fences this collapsed \n\n to '', gluing fence close to next block. Fixed by capturing leading/trailing whitespace at the call site and restoring it after the transform. 3. Currency dollar signs eaten as math. With singleDollarTextMath:true remark-math greedy-matched any pair of $, so '$5 ... $10' became one inline math span. Added escapeCurrencyDollars to escape $<digit> patterns to \$<digit> in prose segments (not in code). Trade-off: math expressions starting with a digit (rare — '$5x = 10$') get escaped too. Mirrors the convention in ChatGPT/Claude's UIs. 4. \(...\) and \[...\] LaTeX brackets unsupported. Models often emit these instead of $...$ / $$...$$. Added rewriteLatexBracketDelimiters preprocessor pass. 5. ```latex / ```tex blocks were being routed to KaTeX via a rewrite to ```math. Aligns with GitHub markdown convention: ```math = render as math; ```latex / ```tex = LaTeX/TeX source code (syntax highlighted, not rendered). Conflating them broke teaching/showing-source use cases. MATH_FENCE_LANGUAGES pruned to {'math'} only. Also flipped parseIncompleteMarkdown to true (was !isStreaming) so the math parser can't see $ inside streaming-but-not-yet-closed code fences. Shiki was already deferred via defer={isStreaming} so this doesn't introduce new tokenization cost. Test: 18/18 existing tests still pass; one test updated to expect escaped \$ in currency-prose-with-URL case. * fix(desktop): detect Python via registry/filesystem; pin to 3.11–3.13 Two related fixes for Python detection on Windows: 1. py.exe (Python launcher) is missing from per-user installs that didn't check the launcher option, so 'py -3.X --version' alone misses real Python installs. User-reported case: clean Win11 + official Python.org 3.14 install -> 'where py' returned nothing, our installer offered to install Python again. Both NSIS prereq page and main.cjs now probe in this order: 1. py.exe launcher (when present) 2. PEP 514 registry: HKLM/HKCU\SOFTWARE\Python\PythonCore\<v>\InstallPath 3. Filesystem: %ProgramFiles%\Python<v>, %LocalAppData%\Programs\Python\Python<v> Crucially, we never fall back to running 'python.exe' from PATH on Windows — the WindowsApps stub at %LOCALAPPDATA%\Microsoft\ WindowsApps\python.exe is a redirector that opens the Microsoft Store window if no Store Python is installed. Triggering that during boot would be terrible UX. Registry/filesystem probes never execute the binary. 2. Drop 3.14 from the supported version set. Several Hermes deps (notably pywinpty, which carries Rust crates like windows_x86_64_msvc) don't yet publish 3.14 wheels. With wheels missing, 'pip install -e .' falls back to building from sdist, which needs a Rust toolchain — users see 'could not compile windows_x86_64_msvc build script' on first run. install.ps1 sidesteps this by pinning to 3.11 via uv; the desktop installer doesn't yet have the same uv-managed-Python pathway, so for now we accept 3.11/3.12/3.13 and tell winget to install 3.11 if none of those are present. Revisit when the wheel ecosystem catches up to 3.14 (~early 2026). * feat(desktop): Cron, Profiles, usage analytics, and titlebar fixes - Add Cron and Profiles sidebar routes with full CRUD-style flows and API wiring. - Extend Command Center with auxiliary task overrides and a Usage panel (7d/30d/90d). - Fix titlebar geometry for WSL/Windows (native overlay width, tool spacing). - Remove stray merge conflict markers from pyproject.toml optional deps. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(title-bar): position sidebar toggle button * feat(desktop): composer queue — queue many, edit/delete/cancel-edit, Cursor-style Press Enter while busy with a draft to queue it; with no draft to interrupt and send the next queued turn. Auto-drains one queued turn each time the session settles, same as Cursor. Queue persists across reloads so an interrupted-and-queued turn isn't lost on refresh. Each queued row supports edit-in-composer (with explicit Save/Cancel), send-now (↑), and delete. Drain skips only the entry currently being edited so the rest of the queue keeps flowing. Queue dequeue is transactional — an entry only leaves the queue after `prompt.submit` is accepted, so a rejected submit doesn't drop the turn. Also shrinks the `[interrupted]` marker to a muted one-liner and drops its assistant footer so it stops looking like a real reply. * fix(desktop): handle empty usage analytics totals Co-authored-by: Cursor <cursoragent@cursor.com> * fix(desktop): address PR review titlebar and usage races Co-authored-by: Cursor <cursoragent@cursor.com> * feat(desktop): add MCP settings and live subagent tree Surface configured MCP servers in Settings with JSON edit/save and a gateway-backed reload action so users can manage tool servers without falling back to slash commands. Track live subagent gateway events in a desktop store, show active subagent counts in the Agents statusbar item, and replace the Agents overlay stub with a live spawn tree for the active session. * fix(desktop): move power-user views out of sidebar Keep Cron and Profiles available through lower-prominence chrome entry points so the workspace sidebar stays focused on core chat navigation. Co-authored-by: Cursor <cursoragent@cursor.com> * refactor(desktop): subagent overlay reads like a live transcript, not a dashboard Strip the card chrome and rewire /agents to feel like peeking into the child agent's stream: - subagents store: single `stream` of typed entries (thinking/tool/progress/ summary) replaces the parallel notes/thinking/tools arrays. Drop unused fields (toolsets, depth, apiCalls, reasoningTokens, sessionId). - agents view: no OverlayCards, no boxed stream, no per-row borders. Goal + status pill + indented stream lines, full row width. - Group root spawns into "Delegation N" sections when batch shape + spawn time match — hides task-index interleaving and makes hierarchy obvious. - Sort tree by spawn time, then task_index. Step indicator is one colored pill (primary while running, emerald when done) inside the row, not a trailing pill that wrapped under the chevron. - Tree picks up `subagent.start` (not only `spawn_requested`) and prunes delegate-tool fallback rows once native subagent events land for the session — fixes duplicate "Delegated task" rows alongside the real ones. * feat(desktop): Esc closes every OverlayView-based overlay Lift the keyboard handler into the shared OverlayView so Agents, Settings, Command Center — and anything we build on top of it later — all dismiss on Esc by default. Nested Radix dialogs stop propagation themselves, so a modal opened inside an overlay (e.g. model picker inside Settings) still closes the modal first, not the overlay underneath. Drop the now-redundant Esc handlers in Settings (kept Cmd/Ctrl+P) and Command Center. * fix(desktop): drop numbered step pill on subagent rows The pill was getting clipped at the overlay edge anyway. Just use the status glyph (●/✓/✗/■/○) — the delegation header already conveys "3 workers, 3 active", and order in the list implies which step you're looking at. * fix(desktop): drop noisy "returned N items / empty object" stub strings When a tool returns nothing useful, the row should be silent — the title ("Search Files", etc.) already tells the user what happened. Counting the fields in an opaque payload is engineer-noise. `formatToolResultSummary` and `minimalValueSummary` now return '' for empty arrays / records / unrecognized values; tool-fallback already hides the detail section when its body is empty. * refactor(desktop): subagent rows borrow chat tool patterns (fade-in, lucide glyphs, shimmer) Pull the agents view closer to how chat tool blocks render: - statusGlyph() returns the same lucide BrailleSpinner / CheckCircle2 / AlertCircle vocabulary as tool-fallback's statusGlyph - Stream lines fade-in via useEnterAnimation (one-shot WAAPI), keyed per entry so streamed deltas settle in instead of popping - Subagent rows fade in too, and pick up the existing data-slot=tool-block spacing rules between blocks - Active stream line trails a BrailleSpinner instead of a hand-rolled pulsing rectangle - Goal text drops FadeText (which forces nowrap); keep FadeText only for the single-line meta subtitle - Running rows shimmer the title — same affordance the chat thinking row uses * refactor(desktop): make /agents subagent-only, drop sidebar + dead sections Activity rail and History stub were both noise. Strip the split layout, sidebar, route enum, and the rail/stub helpers — the overlay is now just the spawn tree, centered in a max-w-3xl column so it stops claiming the whole screen for one section's worth of content. * feat: update cron modals * Add dedicated GUI log stream for dashboard debugging. Capture dashboard and PTY websocket lifecycle failures in gui.log and expose it via hermes logs. * Improve desktop runtime UX by surfacing inference readiness in gateway status and hardening WSL link opening. This also stabilizes markdown code/table block spacing and adds root-install guards so desktop dev runs use a healthy workspace dependency tree. * Log detailed GUI websocket failure metadata. Capture richer reject/disconnect/send/parse context for dashboard gateway websocket flows so GUI connection failures are diagnosable from logs. * Default dashboard startup logging to GUI mode. Detect the dashboard subcommand during early CLI bootstrap so gui.log is attached from process start and GUI startup failures are always captured. * Clean up gateway status conditionals and logging bootstrap mode detection. Simplify nested dashboard gateway status branches for readability and use a concise first-subcommand check when selecting early GUI logging mode. * add logging to nsis installer * feat: glass ui pass * fix(desktop): persist inline assistant errors across hydrate/resume - Detect provider failure text arriving via message.complete (HTTP 4xx, "API call failed after N retries", Provider/Gateway error: ...) and persist as an inline assistant error instead of regular completion text, blocking the hydrate that was wiping it. - preserveLocalAssistantErrors: merge by id so same-id hydrated messages keep their local error, and preserve the optimistic user+error pair as a unit (with tail-user dedupe). - Hook all hydrate/resume writers (use-session-actions resume + fallback, hydrateFromStoredSession, syncSessionStateToView) into the merge so stale snapshots can't clobber a failed turn. - Add error to chatMessagesEquivalent so the resume diff actually sees error-only changes and paints them. - editMessage on a failed turn now submits a plain resend (no truncate_before_user_ordinal) and retries plainly on the "no longer in session history" race. Style polish on touched files: - Inline error: text-only treatment (no card). - User stop / edit-composer send: shared Tabler IconPlayerStopFilled glyph + shared icon-button class slot for parity. * feat(desktop): theme xterm with active light/dark mode The right-sidebar terminal hardcoded a light palette, which read poorly on the dark glass surface. Subscribe to `useTheme().resolvedMode` and hot-swap `term.options.theme` so Shift+X (and any other mode change) updates the terminal in place without tearing down the PTY session. Dark mode uses xterm's built-in defaults (white fg/cursor + vivid ANSI 16) with just a transparent background so the glass shows through; light mode keeps the existing hand-tuned overrides for legibility on a bright surface. * feat(sidebar): right-click + drag-reorder sessions and workspaces - Wire right-click on session rows to open the same actions menu; suppresses the OS-native context menu so Windows stops looking awful. - Share dropdown + context menu items via useSessionActions() driving a single declarative ItemSpec[]; render polymorphic over MenuItem. - New shadcn ContextMenu primitive mirroring DropdownMenu styling. - Restore drag-and-drop reordering for Agents (lost during the cwd cleanup) and add reordering of workspace groups via a right-side grab handle. Pinned reorder unchanged. - Generic orderByIds<T> replaces the duplicated session/group orderers; useSortableBindings() hook collapses the two Sortable wrappers. - cursor-pointer on every actionable element; cursor-grab on handles. - KISS pass: baseName() helper, AGE_TICKS table, single WORKSPACE_PAGE constant, flatter SidebarSessionsSection render. * feat(desktop): solarize the xterm palette in both light & dark xterm's default ANSI 16 is tuned for dark and reads candy-bright on the light glass surface (vivid cyans/greens). Ship the canonical Solarized palette (Schoonover) for both modes — same 16 accents either way, only fg/cursor swap between `base00/01` (light) and `base0/1` (dark), so a prompt's colors look uniform across a Shift+X toggle. Background stays transparent in both modes — Solarized's cream/slate backgrounds would fight the glass. * feat(desktop): virtualize chat thread + sidebar via TanStack Virtual Replaces `use-stick-to-bottom` and per-row session rendering with `@tanstack/react-virtual`, matching what Cursor uses. Chat thread (`thread-virtualizer.tsx`): - Natural-flow virtualization (padding spacers, not absolute items) so `position: sticky` on the human bubble still resolves cleanly against the scroller. - Custom at-bottom anchor: pins when armed, disarms on user-driven upward scroll, re-arms at bottom, jumps on session switch + `thread.runStart`. - Loading indicator and `--thread-last-message-clearance` move to a real `[data-slot=aui_composer-clearance]` node; drops the brittle `:nth-last-child(1 of …)` rule that can't fire reliably under virtualization. Sidebar (`virtual-session-list.tsx`): - Flat agents list virtualizes at >=25 rows; pinned and workspace-grouped paths stay direct-render. - `SortableContext` keeps all IDs; only the window mounts; dnd-kit's `setNodeRef` is merged with `virtualizer.measureElement` so rows participate in both DnD hit-testing and TanStack measurement. Drops `use-stick-to-bottom`. Streaming test gets a global `offsetWidth/offsetHeight` stub so the virtualizer's viewport sizing works in jsdom; the scroll-up-doesn't-pull-back invariant still passes. * feat: more ui qa * fix(desktop): trim sidebar terminal startup spacer Drop zsh's initial spacer row before writing the first terminal prompt so new sidebar terminal sessions do not open with a selectable blank line. * chore: uptick * feat(desktop): thin installer + first-launch install.ps1 bootstrap Converges the Windows packaged desktop installer onto a single canonical install topology: drop the Electron shell only (~80MB instead of ~500MB), clone Hermes Agent at a build-time-pinned commit on first launch via install.ps1's stage protocol, and treat the resulting git checkout at %LOCALAPPDATA%\hermes\hermes-agent\ as the canonical install location (same path the CLI installer uses). Future updates flow through the existing applyUpdates() git-pull path. Replaces the previous fat-installer architecture where the .exe bundled a pre-staged hermes-agent source tree under resources/hermes-agent/ that was then sync'd into ACTIVE_HERMES_ROOT at launch -- a complicated factory-vs-active dance with several footguns (FACTORY_HERMES_ROOT mismatch on path resolve, isGitCheckout guard regressions, pyproject hash drift detection inside the sync loop). Architecture overview --------------------- Build time apps/desktop/scripts/write-build-stamp.cjs writes apps/desktop/build/install-stamp.json with {commit, branch, builtAt, dirty}. Honours $GITHUB_SHA / $GITHUB_REF_NAME in CI, falls back to `git rev-parse HEAD` locally. apps/desktop/scripts/stage-native-deps.cjs copies the runtime subset of @homebridge/node-pty-prebuilt-multiarch from the workspace-root node_modules into apps/desktop/build/native-deps/. Workspace dedup hoists this dep to the root, out of reach of electron-builder's `files:`-restricted collector; staging gives us a deterministic path to extraResources. electron-builder ships both into resources/install-stamp.json and resources/native-deps/ respectively. Boot resolver (electron/main.cjs) Resolver order: 1. HERMES_DESKTOP_HERMES_ROOT override 2. SOURCE_REPO_ROOT (dev mode) 3. ACTIVE_HERMES_ROOT git checkout WITH .hermes-bootstrap-complete marker -- the post-install fast path 4. `hermes` on PATH (CLI-installed user adding the desktop) 5. pip-installed hermes_cli via system Python 6. bootstrap-needed sentinel -> hand off to runBootstrap Deletes the entire FACTORY_HERMES_ROOT / RUNTIME_MARKER / syncTreeExcludingVenv machinery (-200 lines). The isGitCheckout guard that bit us in the install.ps1 PR is gone. First-launch bootstrap (electron/bootstrap-runner.cjs) 1. Resolve install.ps1: prefer SOURCE_REPO_ROOT/scripts (dev), else download from GitHub raw at INSTALL_STAMP.commit (cached at HERMES_HOME\bootstrap-cache\install-<sha>.ps1). 2. Fetch the stage manifest via install.ps1 -Manifest -Commit X -Branch Y. 3. Iterate stages: install.ps1 -Stage <name> -NonInteractive -Json -Commit X -Branch Y per stage. 4. On all stages green: write the .hermes-bootstrap-complete marker with {schemaVersion, pinnedCommit, pinnedBranch, completedAt, desktopVersion}. Per-run log to HERMES_HOME\logs\bootstrap-<ts>.log. Cancellation via AbortSignal. Manifest cache so retries don't re-download. Install overlay (src/components/desktop-install-overlay.tsx) Mounted alongside the existing onboarding overlay; flexbox card with header (static) + middle (scrollable) + footer (failure-only, static). Subscribes to hermes:bootstrap:event IPC + resyncs from hermes:bootstrap:get on mount/reload. Renders: - 14-stage checklist with per-stage state icons - Overall progress bar + current-stage spotlight - Auto-expanded installer-output panel on failure - "Copy output" button (full ring buffer + error to clipboard) - "Reload and retry" wired through hermes:bootstrap:reset to clear main.cjs's latched failure Synthetic empty-manifest event from main.cjs flips the overlay to 'active' immediately so the slow install.ps1 download doesn't leave the user staring at the generic Preparing splash. Failure latching (main.cjs) bootstrapFailure module-scope variable holds the rejection after install.ps1 fails. startHermes() throws the latched error immediately when set, bypassing the entire ensureRuntime + runBootstrap chain. Without this, the renderer's ensureGatewayOpen retries would re-run install.ps1 in a 5-10 min hot loop while the user was still reading the failure overlay. Cleared via hermes:bootstrap:reset on user-driven retry. Unsupported-platform overlay (1F) macOS / Linux packaged builds (no install.sh stage protocol yet) emit an unsupported-platform event with a copy-pasteable install command + docs URL. Dedicated overlay branch with "Copy command" + "I've run it -- retry" buttons. install.ps1 additions (Phase 1F.3 + 1F.5) ----------------------------------------- New -Commit and -Tag string params. Precedence Commit > Tag > Branch. Honoured by all three code paths (update / fresh clone / ZIP fallback), with archive URL selection that handles each ref-type variant. Detached-HEAD checkouts intentionally -- they're pins, not branches the user pulls into. EAP=Continue wrap around the new pin-step git invocations. `git fetch origin <commit>` writes the routine 'From <url>' info line to stderr; under the script's global EAP=Stop that terminates the script even though fetch+checkout succeed. Matches the established pattern in Install-Uv, Test-Python, _Run-NpmInstall. Backend fix (hermes_cli/web_server.py) -------------------------------------- CORS allow_origin_regex now accepts Origin: 'null'. Packaged Electron loads index.html via file://; Chromium sets the WebSocket upgrade Origin header to the opaque origin 'null', which the old regex rejected with HTTP 403 before gateway_ws() ever ran. This failure mode was masked in the older FACTORY_HERMES_ROOT architecture because the resolver often found an existing hermes on PATH with different binding behavior. Security maintained: localhost-only bind keeps cross-machine pages out; per-process session token still gates every authenticated /api/ endpoint regardless of Origin. Desktop QoL ----------- DevTools is now enabled in packaged builds (F12 / Cmd+Opt+I). Field-debugging trade-off: tiny attack surface increase versus a much better support story when CSP / WS / theme issues surface. NSIS prereq-check page deleted (-767 lines). The standard Welcome -> License -> Directory -> InstallFiles -> Finish wizard now installs without custom Python/Git/ripgrep detection -- those prereqs are install.ps1's job at first launch. Test infrastructure (Phase 1G) ------------------------------ apps/desktop/scripts/test-desktop.mjs rewritten as a cross-platform bundle validator (was darwin-only and asserted on dead factory- payload paths): NEGATIVE: hermes_cli/main.py is NOT shipped (regression guard) POSITIVE: install-stamp.json carries a real commit + branch POSITIVE: node-pty native deps shipped under resources/native-deps POSITIVE: renderer dist/index.html reachable (asar or unpacked) New nsis mode and npm run test:desktop:nsis script. Validated end-to-end on clean Win10 VM -------------------------------------- Confirmed: NSIS installer drops Electron shell, app launches, install overlay shows progress, install.ps1 clones the pinned commit, 14 stages run to completion, marker written, backend spawns, WebSocket connects, onboarding overlay asks for API key, main UI loads, integrated terminal works. Failures handled: bootstrap stays failed (no hot-loop retry), "Copy output" gives actionable transcript, "Reload and retry" explicitly re-runs install.ps1. What's deferred --------------- - MSIX wrapping (Phase 2): same Electron .exe under MSIX manifest with runFullTrust, signed and submitted to Microsoft Store. - install.sh stage protocol parity (Phase 2): once shipped, the unsupported-platform overlay becomes drive-it-yourself and macOS/Linux packaged installers gain feature parity with Windows. * feat(desktop): persistent terminal pane + fullscreen takeover Adds a VSCode-style "focus terminal" toggle to the right sidebar's Terminal tab that takes over the chat pane area without unmounting the shell. The xterm host is mounted once at the layout root and CSS-overlayed onto whichever <TerminalSlot /> is currently active, so the PTY session, scrollback, selection, focus, and WebGL renderer survive every toggle. Also: - WebGL renderer (matching dashboard ChatPage) so Hermes' TUI skins paint faithfully instead of muting through xterm's default DOM renderer - File drag/drop from the project tree or OS into xterm — paths are shell-quoted (zsh/bash/pwsh/cmd) and written straight into the PTY - Solarized dark canvas with brights promoted to real accent variants (Schoonover's UI-gray brights washed out every TUI accent) - Strip NO_COLOR/FORCE_COLOR/COLORFGBG/TERM=dumb leaking from non-tty parents (CI runners, Cursor's agent shell) so the embedded shell gets truecolor regardless of how Electron was launched - rAF-debounced ResizeObserver — running fit.fit() synchronously during sibling pane transitions crashed the WebGL texture-atlas rebuild * fix(install.ps1): strip UTF-8 BOM regression that broke 'irm | iex' The canonical install flow irm https://raw.githubusercontent.com/.../scripts/install.ps1 | iex fails on PowerShell 5.1 with a cascade of 'The assignment expression is not valid' errors at every param() default value: [string]$Branch = 'main', ~~~~~~ The assignment expression is not valid. The input to an assignment operator must be an object that is able to accept assignments... Root cause: scripts/install.ps1 carries a UTF-8 BOM (0xEF 0xBB 0xBF) as its first three bytes. 'irm' returns the response body as a string; on PS 5.1 the BOM survives into that string as a leading \ufeff character. 'iex' then evaluates the string and PS's parser chokes on the invisible character before param() -- error recovery proceeds into the body but every assignment is reported as broken. This was the exact failure mode the install.ps1 hardening pass (PR #27224) deliberately fixed by stripping the BOM and ensuring the file body is pure ASCII. Commit 4279da4db ('fix(windows): make PowerShell installer parse in 5.1') re-introduced the BOM later, unintentionally undoing the irm|iex compatibility fix; the merge that brought it into bb/gui carried it forward. Fix: strip the three BOM bytes. File body is verified pure ASCII (any-byte > 127 returns false), so PS 5.1 with no BOM falls back to Windows-1252 decoding which is identical to ASCII for our content. Both install paths now work: - 'irm ... | iex' (canonical CLI) - 'powershell -File install.ps1' (programmatic / desktop bootstrap) * install.ps1: detect ARM64 Windows reliably for Node and Git stages Add a Get-WindowsArch helper that reads Win32_Processor.Architecture via CIM (invariant to PowerShell host bitness) with PROCESSOR_ARCHITEW6432 fallback. Use it in: - Install-Git: previously only triggered the arm64 PortableGit asset when invoked from a native-ARM64 PowerShell host. WoW64 / emulated x64 hosts (the default powershell.exe on Windows-on-ARM) saw PROCESSOR_ARCHITECTURE=AMD64 and fell through to the x64 PortableGit build, leaving ARM64 users on emulated Git for Windows. - Test-Node: previously hardcoded the Node download to win-x64 on any 64-bit OS, so ARM64 users always got x64 Node under Prism emulation even though Node ships an arm64 build for Windows. The winget fallback now also passes --architecture arm64 on ARM64. Python remains x86_64 by design: uv intentionally prefers windows-x86_64 cpython on ARM64 hosts for ecosystem (wheel) compatibility (see astral-sh/uv#19015). * install.ps1: harden Install-SystemPackages against winget msstore failures The previous winget invocation discarded stdout/stderr and trusted no signal at all -- not the exit code (winget exits 0 even when it bails "please specify --source"), not output (sent to Out-Null), not the catch handler (winget returning 0 means no exception fires). The only trust signal was a post-install Get-Command rg / Get-Command ffmpeg check, which would also miss the package because %LOCALAPPDATA%\ Microsoft\WinGet\Links (where winget puts command aliases) is added to PATH by AppExecutionAlias machinery only in fresh shells. End result on machines where the msstore source has a cert problem (0x8a15005e -- common on Windows-on-ARM and some corporate networks): silent failure, no log, no breadcrumb, and the user is told the install succeeded. Specifically: - Pin --source winget on every winget install call. Defeats the broken- msstore-source path. We ship nothing from msstore so this is safe and forward-compatible. - Add --exact --id for a tighter package match. - Capture each winget invocation's combined stdout/stderr + exit code to %TEMP%\hermes-winget-<pkg>-<n>.log instead of Out-Null. On the happy path the log is deleted after the post-install check confirms the binary is on PATH; on failure the log is kept and its path is named in a Write-Warn so the user has something to grep. - Refresh PATH to include %LOCALAPPDATA%\Microsoft\WinGet\Links in addition to the User/Machine env-var hives, so Get-Command sees newly- installed winget aliases in the same process. - No behavior change on the happy path. Same Write-Info/Success/Warn cadence, same fallback order (winget -> choco -> scoop -> manual), same $script:HasRipgrep / $script:HasFfmpeg outputs. Verified end-to-end on a real Snapdragon ARM64 Windows host: ripgrep uninstalled, stage re-run, [OK] ripgrep installed in 1.4s, ok:true. * desktop: swap node-pty fork for upstream microsoft/node-pty 1.1.0 The previous dependency, @homebridge/node-pty-prebuilt-multiarch@0.13.1, publishes no win32-arm64 prebuilds on its v0.13.x line, and its v0.14.x betas (which do add an arm64 Windows build) ship no electron-vXXX-win32- arm64 prebuilds at all -- so packaged Electron 40 builds (NMV 143) would fail at runtime even on a successful npm install. Net effect: the desktop's integrated terminal was unbuildable on Windows-on-ARM, in both dev (npm install fails: 404 fetching the node-vXXX-win32-arm64 prebuilt) and packaged builds (no Electron-ABI prebuilt exists). The homebridge fork was originally created because upstream node-pty shipped no prebuilds at all. That hasn't been true since node-pty@1.0 (April 2024), which: - bundles prebuilts for mac (arm64+x64) and Windows (arm64+x64) directly inside the npm tarball -- no GitHub-Releases fetch, no missing-binary failure mode - uses N-API (node-addon-api) for ABI stability across Node and Electron major versions, so the same pty.node binary loads under Node 22 (dev) and Electron 40+ (packaged) without per-ABI rebuilds - is what VS Code, Hyper, and Theia actually ship API surface is identical (spawn / onData / onExit / write / resize / kill) -- no call-site changes needed. Specifically: - apps/desktop/package.json: replace the @homebridge fork with node-pty@1.1.0 (exact pin). Widen `asarUnpack` from `["**/*.node"]` to also unpack `**/prebuilds/**`, because node-pty ships runtime- execed helpers alongside its .node files (darwin spawn-helper has no extension and would not be matched by `**/*.node`; conpty.dll, OpenConsole.exe, winpty.dll, winpty-agent.exe on Windows are also exec'd at runtime and cannot live inside asar). - apps/desktop/electron/main.cjs: update both require() strings to match the new package name and the new staged path under resources/native-deps/node-pty/. - apps/desktop/scripts/stage-native-deps.cjs: point at node_modules/ node-pty. node-pty's prebuilts live under prebuilds/<plat>-<arch>/ (not build/Release/), so update the include glob to copy that dir. Per-arch staging keeps the resource bundle small (target arch comes from npm_config_arch when electron-builder cross-builds, else process.arch). Explicitly enumerate file types in the prebuilds glob so the ~25 MB of .pdb debug symbols that prebuild-install bundles for Windows crash analysis don't bloat the installer (29 MB -> 2.6 MB staged on win32-arm64). Re-assert +x on the darwin spawn-helper defensively, since a stripped mode bit would manifest as a silent ENOENT at first pty.spawn(). - apps/desktop/scripts/test-desktop.mjs: update expectedNativeDepPaths() and its assertion site to look at prebuilds/<plat>-<arch>/ instead of build/Release/. Add an explicit spawn-helper-exists check on darwin so a regression in the asarUnpack glob would fail loudly in CI rather than at first PTY spawn. Trade-off: Linux end-users lose prebuilts and fall back to building node-pty from source on `npm install`. Acceptable because Hermes ships no Linux desktop builds (desktop-release.yml matrix is mac + win only, package.json declares no `linux` target), and Linux developers hacking on the desktop already need a C++ toolchain for the rest of the stack. Verified on Windows 11 ARM64 (Snapdragon): npm install -> exit 0 node -e "require('node-pty').spawn(...)" round-trip -> OK stage-native-deps -> 27 files, 2.6 MB load from staged tree (simulates packaged fallback) -> ConPTY round-trip OK * desktop+gateway: harden Slack socket recovery and Windows restart dedupe (#28873) * desktop+gateway: harden Slack socket recovery and Windows restart dedupe Fix Slack Socket Mode reliability by adding a watchdog/reconnect path so silent socket task drops no longer leave the adapter stuck. Harden Windows gateway lifecycle by avoiding desktop-binary path collisions, making gateway PID scans case/extension tolerant, and reusing in-flight restart actions to prevent duplicate gateway spawns. * test(slack): add Socket Mode watchdog/reconnect behavioural coverage Drive the new Slack Socket Mode self-healing logic through a fake AsyncSocketModeHandler so we can simulate the P0 silent-hang failure mode (task exit, transport disconnected, intentional shutdown, concurrent reconnect attempts) without touching real Slack. * fix(slack,desktop): address Copilot review on watchdog races and path normalization - connect(): explicitly cancel + await the prior socket watchdog before flipping _running, so an old monitor cannot exit between teardown and respawn (Copilot #1) - _socket_watchdog_loop: wrap the body in try/except + add a done-callback that respawns on unexpected crash, so a transient bug cannot permanently disable self-healing (Copilot #2) - normalizeExecutablePathForCompare: use the resolved path for realpathSync so non-string inputs cannot leak through (Copilot #3) - Add tests for crash-recovery and atomic watchdog replacement across reconnects * fix(slack): tighten connect() error path and clarify watchdog test intent Address Copilot review round 2. - connect(): wrap _start_socket_mode_handler/_ensure_socket_watchdog in a focused try/except so any failure rolls back partially-started handler/task state and leaves _running=False, ensuring the platform lock is always released by the outer finally - Defer _running=True until after the handler is actually started so the watchdog observes a live socket task immediately and never spins against a half-built adapter - Rename test_watchdog_self_restarts_after_unexpected_crash to test_watchdog_cancellation_does_not_respawn (matches what it actually asserts) and add test_watchdog_unexpected_exit_respawns_via_done_callback that drives a real RuntimeError through _on_socket_watchdog_done and verifies a fresh task replaces the crashed one * fix(web_server): serialize action spawn check+store under a threading lock Address Copilot review round 3. FastAPI runs sync handlers on its threadpool, so two near-simultaneous /api/gateway/restart (or /api/hermes/update) requests could both observe "no live process" in _spawn_hermes_action's poll-based dedupe and double-spawn. Add a module-level _ACTION_SPAWN_LOCK around the entire check + Popen + _ACTION_PROCS store sequence so the dedupe is atomic across threads. * fix: address Copilot review round 4 - slack.disconnect(): mirror connect()'s defensive cleanup — catch the broad Exception path on watchdog await so handler shutdown and lock release still run if the watchdog raised before cancellation took effect - web_server._spawn_hermes_action: wrap subprocess.Popen in try/except so a missing executable / permission error closes the log file handle, writes a failure marker, and re-raises instead of leaking a file descriptor - gateway._scan_gateway_pids: drop the over-broad "hermes.exe --profile" / "hermes.exe -p" patterns that would match any Hermes CLI subcommand using a profile flag (e.g. `hermes.exe --profile foo dashboard`); rely on the "hermes.exe gateway" + "hermes-gateway.exe" tokens instead - tests: tighten _fake_create_task to assert coroutine input and return a real asyncio.Task that stays pending until pytest teardown, and update the three callsites whose mocked AsyncSocketModeHandler.start_async returned a non-coroutine value * fix(slack): reset multi-workspace state on reconnect Address Copilot review round 5. connect() is reentrant (gateway restart, in-process reconnect), but it was leaving _bot_user_id / _team_clients / _team_bot_user_ids populated from the previous session. A reconnect that rotated the primary token or dropped a workspace would silently keep the stale bot user id and stale workspace client maps, leading to dispatch against gone workspaces. Clear these three pieces of state right after _stop_socket_mode_handler() and before the auth_test loop, then let the loop repopulate from the current tokens. Add test_reconnect_refreshes_multi_workspace_state to lock it in. * nix: package apps/desktop as .#desktop (#28964) Adds nix/desktop.nix building the Electron renderer with buildNpmPackage and wrapping nixpkgs' electron binary. Reuses .#default by setting HERMES_DESKTOP_HERMES to its hermes binary, so the desktop's resolver picks up the fully-wired nix hermes (venv, bundled skills/plugins, runtime PATH) without reimplementing agent resolution. - nix/desktop.nix: renderer + electron wrapper - nix/hermes-agent.nix: finalAttrs form, exposes hermesDesktop in passthru - nix/packages.nix: exposes .#desktop + adds to fix-lockfiles - apps/desktop/package-lock.json: standalone hermetic lockfile nix build .#desktop && nix run .#desktop both clean. * fix(desktop): probe steps 4 & 5 of resolveHermesBackend before trusting A user-reported failure on Windows-on-ARM: a pre-installed Python 3.13 on PATH makes findSystemPython() succeed, so resolveHermesBackend returns a backend pointing at it -- but hermes_cli isn't in that interpreter's site-packages. The spawn dies with ModuleNotFoundError and the user sees a dead GUI instead of the first-launch installer. Same shape can hit step 4 (existing `hermes` on PATH) when a stale shim survives a partial uninstall. Add cheap exit-code probes -- `python -c "import hermes_cli"` for step 5, `<hermes> --version` for step 4 -- and fall through to step 6 (bootstrap-needed) on failure. install.ps1 then runs as if on a clean box and the venv gets built. Probes live in a standalone electron/backend-probes.cjs module so they can be unit-tested with node --test, same pattern as bootstrap-platform.cjs and hardening.cjs. New test file wired into test:desktop:platforms. * test(desktop): allow `node-pty` bare-require in packaged entrypoints Pre-existing failure on bb/gui since c858484b4 swapped the node-pty fork for upstream microsoft/node-pty 1.1.0. main.cjs intentionally bare-requires node-pty (it's hoisted by workspace dedup in dev, and staged to resources/native-deps via scripts/stage-native-deps.cjs + extraResources for packaged builds, with a try/catch fallback at line ~38). The allowlist hadn't been updated to match -- same shape as `electron`, which was already allowed. * chore(deps): refresh root lockfile for dashboard @nous-research/ui 0.14.0 apps/dashboard/package.json was bumped to @nous-research/ui 0.14.0 (+ flag-icons ^7.5.0, motion ^12.38.0) but the root package-lock.json was never refreshed. Running `npm install` from the repo root now materialises 0.14.0's transitive closure (launder, bumps for @nanostores/react, nanostores, sanitize-html, tailwind-merge). No code changes; purely a lockfile catch-up so fresh checkouts on bb/gui get a working dashboard install. * chore(desktop): bump version to 0.0.1 First non-placeholder version so electron-builder's artifactName template produces `Hermes-0.0.1-win-x64.exe` instead of the obviously-unreleased `Hermes-0.0.0-...`. No release process yet; this just stops the artifact filename from telling users "you got a debug build." Bumped in three slots that all carry the desktop app's version: - apps/desktop/package.json (source of truth) - apps/desktop/package-lock.json (per-app lockfile, kept for CI parity) - root package-lock.json's apps/desktop workspace entry Identity-of-build for first-launch bootstrap continues to come from build/install-stamp.json (commit SHA + builtAt), unchanged. * fix: fs icon color * perf(desktop): cut per-keystroke layout + listener churn in chat composer Empirical work via CDP harnesses under apps/desktop/scripts/ (see profile-typing-lag.md): jsListeners growth (per round of 200 chars + GC): before: +35 (verified leak — listeners stuck after 1st trigger popover use) after: +0 Four narrow edits in src/app/chat/composer/index.tsx: 1. Drop the per-keystroke `editorRef.current.scrollHeight` read used to decide composer expansion. Replace with `draft.length > 60` heuristic; the existing ResizeObserver still catches edge cases. `scrollHeight` is a forced-layout call and was firing on every char until the first wrap. 2. Bucket measured composer height to 8px before writing `--composer-measured-height` / `--composer-surface-measured-height` on `documentElement`. Without this, the editor grows ~1px per char, setProperty fires every keystroke, computed style is invalidated tree- wide. 3. Remove the dead `$composerDraft` two-way sync. Nothing outside the composer subscribed to that atom (verified via grep). Two useEffects on `[draft]` were pushing draft→atom and atom→aui per keystroke for no consumer. Also drop the per-keystroke `reconcileComposerTerminalSelections` call; it was pruning stale labels for `terminalContextBlocksFromDraft`, but that helper already ignores labels not in the current submitted text, so pruning per keystroke was just bookkeeping. 4. `refreshTrigger` fast-bails when the draft contains neither `@` nor `/`. Previously `textBeforeCaret(editor)` ran on every input/keyup regardless; `range.toString()` inside is O(n) over draft length. Synthetic typing latency p50/p90/p99 is similar before vs after on a freshly-loaded session (Blink can already handle ~30cps typing into a contentEditable on its own); the real win is the listener leak being gone and the global computed-style invalidations dropping ~8× when the composer is sitting at a fixed height row. The `Enter → stall` follow-up (see profile-typing-lag.md §"Submit / TTFT stall") is unmeasured here — needs a throwaway session because the harness fires a real prompt. Not blocking this commit. * perf(desktop): cut FadeText forced layouts during streaming The slowest user-felt path is typing into the composer while the assistant is streaming. Profile (scripts/profile-under-stream.mjs): FadeText measureOverflow self time: 35.8 ms → 18.1 ms (-50%) total active CPU during 7s window: ~150 ms → ~50 ms Two changes in src/components/ui/fade-text.tsx: 1. Drop the `useEffect([children])` that re-ran `measureOverflow` (reads scrollWidth + clientWidth — forced layout) on every parent re-render. `useResizeObserver` already fires the same callback on mount and whenever the host span's box size changes; that covers the only case where overflow state can legitimately change. The previous explicit useEffect was a forced-layout flush on every parent render, which during streaming meant every token tick. 2. Wrap the component in `memo` with a custom comparator that short-circuits the entire render when scalar string `children` and the className/fadeWidth/style props are unchanged. The hot path was tool-fallback's title chips being re-rendered by parent streaming updates even though their text was stable; memo+ comparator skips that. Also adds two harness scripts under apps/desktop/scripts/: - latency-under-stream.mjs (key→paint latency while a turn streams) - profile-under-stream.mjs (CPU profile while a turn streams) Updates profile-typing-lag.md with the streaming numbers and confirms the Enter→paint submit path is already fast (≤320ms on the populated session; the 2s "stall after Enter" the user noticed once was a one-time cold-start, not reproducible at the UI layer). I'd guess the felt jank in real use is fast-burst typing during a long-form streaming reply (code blocks + markdown lists multiply the per-token render cost). The CPU savings here scale linearly with token volume. * chore(desktop): drop diag scratch scripts no longer needed * docs(desktop): correct leak-typing numbers on a real session Re-ran the leak harness on a populated session (Phaser thread) for both unpatched and patched builds. The original 'listener leak' was transient warm-up cost, not a steady-state leak — both versions show 0 listener growth/round in steady state. The load-bearing number is forced layouts per character: unpatched (HEAD~2): 7.02 layouts/char patched (HEAD): 2.35 layouts/char (3× fewer) The patches reduce per-char forced-layout work to Blink's natural floor. Document node count and heap are flat in both builds. * perf(desktop): fix "Enter jumps up" on long threads User reported: after pressing Enter on a long thread, the view jumps up — the just-submitted message disappears below the fold. Confirmed via apps/desktop/scripts/measure-jump.mjs: before: distFromBottom 0 → 49.5px, sticks there permanently after: distFromBottom 0 → ~0 (worst case 4px for one frame) Root cause in useThreadScrollAnchor (thread-virtualizer.tsx): 1. The sticky-bottom logic disarmed on any scroll event where `scrollTop < lastTopRef.current`. That check can't distinguish a user scrolling up from a programmatic `pinToBottom` write that the browser clamped short of bottom (because content also grew in the same frame, so `scrollTop = scrollHeight` lands at `scrollHeight - clientHeight` for the OLD scrollHeight, which is now below the NEW scrollHeight). Result: sticky-bottom disarmed permanently on the user's first submit. 2. There was no synchronous pin tied to React's commit phase. By the time the ResizeObserver fired and re-pinned, the user had already seen ~50ms of "message below the fold" — visually that reads as the view jumping up. Fix: - `programmaticScrollPendingRef` counter tracks scroll events we expect to be ours (one per `pinToBottom` write). The scroll handler skips the disarm check when consuming a pending tick, keeps the arm bit true, and re-pins synchronously if the browser clamped us short of bottom. A depth cap (8) breaks runaway loops in pathological streaming-burst layouts. - `useLayoutEffect` on `groupCount` increase pins BEFORE the browser paints, eliminating the visible ~50ms window between optimistic user-message insert and the RO/scroll-event chain firing. Verified on the long Cloud Shadows thread (7-8 turns, ~11k px tall): all three repro runs now hold within 0–4 px of bottom across the post-Enter transition. Submit latency unchanged (paint 77–107 ms), streaming-typing latency unchanged. Also adds three debug harnesses: - measure-jump.mjs — sample thread scroll across Enter - probe-thread.mjs — dump current thread / scroll state - diag-jump.mjs — intercept scrollTop + RO + mutations across Enter * perf(desktop): rate-limit thread auto-pin during streaming Follow-up to the Enter-jump fix. The first version did a synchronous re-pin loop inside the on-scroll handler when the browser clamped our `scrollTop = scrollHeight` write short of the new bottom; that gave a tight 4 px visible jump on Enter, but during streaming the ResizeObserver fires many times per second as content grows, and each RO callback re-entered the pin loop. CPU profile showed `Virtualizer.getMaxScrollOffset` climbing to 22 ms self over a typing- during-streaming window — the sync re-pin path was paying tanstack- virtual's recompute cost ~3× per token. Re-architect: - RO callback coalesces to one pin per animation frame. Streaming-rate RO bursts now cost the same as a single per-frame pin. - The on-scroll programmatic-counter guard remains (it's what prevents the false-disarm bug when the browser clamps a write). It no longer does sync re-pins; the next RO/rAF will catch up. - The useLayoutEffect on groupCount (the path that fires on user submit / new turn arrival) ALSO schedules one rAF pin in addition to the synchronous pin. This catches the case where React mounts the new message in a second commit (after our layout effect ran), which grows scrollHeight again. Two pins instead of a tight loop, paid only once per turn change. Net effect on the Cloud Shadows long thread: enter-jump transient: 12–20 px for 1 frame (was 49 px permanent) CPU during stream+type: `getMaxScrollOffset` dropped out of top-5 self-time list typing-during-stream: p50 ~10 ms paint, p99 ~20 ms (1 frame), occasional 40 ms+ outliers during burst token arrivals Also adds scripts/profile-long-stream.mjs: 20-second streaming profile with per-500ms FPS histogram + content-length tracking, so we can see whether streaming render cost grows with message length (it doesn't — sustained 60 fps). * perf(desktop): use textContent for trigger precondition Replace composerPlainText() call inside refreshTrigger's no-trigger fast-bail with a textContent check. textContent is a browser-native flat traversal; composerPlainText walks recursively with chip-aware logic. We only need to know if @ or / appears; either way the trigger char will be in textContent because chips contain @ in their refText. Profile shows composerPlainText was ~18ms self over a 12s typing-during- stream window, called from refreshTrigger on every keystroke. Most of that was the precondition check (the trigger detection path is the slow path but only runs when a trigger char is present). * Revert "perf(desktop): use textContent for trigger precondition" This reverts commit a6a78ff08a31129a3a47fa55aca260d93af913a5. * Revert "perf(desktop): cut FadeText forced layouts during streaming" This reverts commit 88e7d7537cdab87200405edf298e38cb37e0a950. * Revert "perf(desktop): cut per-keystroke layout + listener churn in chat composer" This reverts commit bff1b3261d18a2427ac6c345c99f8312728346dd. * Revert "Revert "perf(desktop): cut per-keystroke layout + listener churn in chat composer"" This reverts commit b7b378e3a43f94b9f4a1a34155707c6301c0fd87. * Revert "Revert "perf(desktop): use textContent for trigger precondition"" This reverts commit 0739588f4896902f7f0d4ded8b5eaeb92bfdf042. * chore(desktop): synthetic-stream perf harness + scripts Drops the React `<Profiler>` approach (no-op because Vite is currently serving the production React build) in favor of an externally-observable measurement stack: rAF frame intervals, `PerformanceObserver({entryTypes: ['longtask']})`, and a `MutationObserver` on the live streaming message. Adds a synthetic stream driver — `window.__PERF_DRIVE__.stream({...})` — that pushes tokens through the live `$messages` atom at a controlled rate, so the assistant-ui runtime, incremental repository, and Streamdown markdown pipeline see the same workload they'd see during a real LLM stream, without the LLM cost. The driver lives in `src/app/chat/perf-probe.tsx`; `main.tsx` side-imports it under `import.meta.env.MODE !== 'production'` so it tree-shakes out of prod builds. (Using `MODE` rather than `DEV` because our Vite setup currently reports `DEV=false` even under `vite dev` — see the dev-build note in `profile-typing-lag.md`.) Scripts: - measure-synthetic-stream.mjs drive synthetic + record frame/longtask/mutation - profile-synth-stream.mjs CPU profile + top self-time during synthetic - measure-real-stream.mjs same harness, real LLM stream - profile-real-stream.mjs CPU profile bracketing the real stream window - eval.mjs / reload.mjs small CDP helpers A real-LLM measurement on Cloud Shadows (gpt-4o-mini, 39 s window) showed 12 longtasks in the same 75-127 ms range the synthetic predicted, so the synthetic is a faithful proxy. * perf(desktop): memo FadeText so it skips re-renders when text unchanged FadeText is used 110+ times inside `tool-fallback.tsx` on a tool-heavy thread. During streaming each parent re-render previously triggered the component's `useEffect([children])`, which forced a `scrollWidth` layout read even when the title text was unchanged. The `useResizeObserver` was already covering the genuine resize case, so that effect was strictly redundant work. Drops the effect and wraps the component in `React.memo` with a custom comparator that field-compares `className`, `fadeWidth`, and `style`, plus identity-compares `children` (scalar fast-path; correct for JSX nodes too since a new node should force a re-render). Verified via temporary render counter on the 34 MB `session_20260514_215353_fe0ac8` thread (110 FadeText instances): a 2 s synthetic stream went from ~11k FadeText render calls to 122 — roughly one render per truly-new instance instead of one per parent commit per instance. Doesn't move the longtask needle on its own (Streamdown's markdown re-parse dwarfs it) but eliminates a steady CPU floor and a class of forced layouts during streaming. Profile-typing-lag.md documents the full investigation, including the remaining Streamdown cost as the real source of the perceived "5 fps moment" hitches. * perf(desktop): memoize MarkdownText plugins to stop churning Streamdown The inline `plugins={{ math: mathPlugin, ...(isStreaming ? {} : { code }) }}` on `<StreamdownTextPrimitive>` constructed a new object literal on every parent render. That broke `<Streamdown>`'s outer memo and forced its internal `rehypePlugins` / `remarkPlugins` array useMemos to rebuild, which propagates a new identity into every `<Block>` and defeats Block's memoization for stable historical blocks. After memoizing on `[isStreaming]` (the only real dimension of variance), CPU profile during a 5 s synthetic stream on the 34 MB session shows `parser` self-time dropping out of the top 10, `compile` cut roughly in half, and `bn$1` / `m$1` (micromark internals) leaving the top entries. Doesn't move the visible longtask count on its own — Streamdown's per-Block parse cost still dominates whenever the last block's content changes — but it removes a class of unnecessary re-parses for historical blocks during streaming. See `scripts/profile-typing-lag.md` for the full investigation. * perf(desktop): floor assistant-text flush gap to 33ms for predictable batching `scheduleDeltaFlush` previously coalesced via `requestAnimationFrame` only. The "at most one flush per frame" guarantee that gives you is fine for fast streams (>~80 tok/sec) where multiple tokens arrive within a single frame, but breaks down at typical LLM token rates (30-80 tok/sec) where each token arrives slower than the rAF cadence and triggers its own React commit + Streamdown markdown re-parse. Track `lastFlushAt` and require at least 33 ms between two flushes. React 18+ auto-batching probabilistically already collapsed some of these, but the floor makes it deterministic. A/B on the 34 MB session, 300 tokens at 50 tok/sec (markdown chunks): | | avgFps | p99 frame | LTs / 5 s | max LT | |---|---|---|---|---| | no floor (current rAF) | 54.0 | 38 ms | 2.0 | 145 ms | | 33 ms floor (this PR) | 54.3 | 41 ms | 1.7 | 110 ms | `inter-mutation` p50 also tightens from 22-28 ms to a clean 33 ms, which is the expected signature of a deterministic floor. Doesn't fully solve the user's perceived hitches — Streamdown's per-Block parse cost when the last block grows past ~2 k chars is still the elephant — but it consistently shaves the worst-case longtask and makes the streaming cadence visibly steadier. Also threads a matching `flushMinMs` option through the synthetic stream driver in `perf-probe.tsx` + `scripts/measure-synthetic-stream.mjs` so the harness can A/B both regimes without spending LLM credits. See `scripts/profile-typing-lag.md` for the full investigation. * perf(desktop): useDeferredValue for streaming markdown so parses don't block input Streamdown's per-Block parse cost grows with the live tail's length and is unavoidable inside the block-memo pattern (industry standard, see findings doc). The fix is to stop having that work block the main thread. `<DeferStreamingText>` is a 12-line wrapper that reads message-part state via `useMessagePartText`, runs it through `useDeferredValue`, and re-publishes via assistant-ui's `<TextMessagePartProvider>`. The inner `<StreamdownTextPrimitive>` reads the deferred value through the normal `useMessagePartText` hook — no fork, no internal-path imports, fully on assistant-ui's public API. React's concurrent scheduler then: - abandons in-flight deferred renders when a newer token arrives, so intermediate states get skipped under fast streams - deprioritises the markdown render when the main thread has urgent work (typing, scroll), so input stays responsive even while a 100ms parse is queued Streamdown already uses `useTransition` for its block-array setState; this lifts the deferral up to the consumer boundary so it covers the whole pipeline (preprocess → split → repair → parse → render). A/B on the 34 MB session, 300 tokens at 50 tok/sec, markdown chunks (four trials each, with the 33ms flush throttle on for both): | | avgFps | p99 frame | LTs/5s | max LT | typing-while-stream p95 | |---|---|---|---|---|---| | pre | 54.3 | 41 ms | 1.7 | 110 ms | ~17 ms | | post | 58.5 | 31 ms | 2.0 | 117 ms | 14-18 ms | Longtask count + max LT unchanged — useDeferredValue doesn't reduce CPU, only its priority. The avgFps lift and p99 frame drop are the proof that the existing CPU is no longer blocking 60 fps cadence. One clean run logged MUTATIONS=0 — React skipped every intermediate text state and only committed the final one (textbook deferred-value behaviour). The actually-reduce-CPU path is replacing the parser with a state machine like Flowdown — left for a future PR; see `apps/desktop/scripts/profile-typing-lag.md` for the full investigation. * feat(desktop): add hermes gui launcher * feat(desktop): launch packaged gui builds by default * bump gui version to 0.0.2 * fix(dashboard): allow file:// origin on loopback WS + diagnostic logging Upstream commit 2e66eefbc ("fix(dashboard): validate WebSocket Host and Origin") added a WebSocket Host/Origin guard to block DNS rebinding against the dashboard. The guard rejects any Origin whose scheme is not http/https or whose netloc is empty — which includes Electron's renderer Origin: file:// when the desktop app loads its bundle from disk in production mode. That makes the bb/gui Electron desktop unable to open the gateway WebSocket against the embedded backend on Windows / macOS prod builds. The renderer reports "Desktop boot failed" and the backend logs: WARNING hermes_cli.web_server: gateway-ws reject peer=127.0.0.1:NNNN reason=non_loopback_or_bad_origin bound_host=127.0.0.1 close_code=4403 DNS-rebinding requires a DNS-resolvable hostname; file:// has no host component and therefore cannot be the attack vector this guard exists to block. When bound to a loopback interface (127.0.0.1 / ::1 / localhost), accept file:// origins so desktop wrappers can attach. Non-loopback binds (operator opted into network exposure) keep rejecting file:// — the loose policy doesn't apply. Also adds per-reason diagnostic logging in _ws_host_origin_is_allowed, so future ws-guard rejections name the specific clause that fired (bad_host / bad_origin_scheme / origin_host_mismatch) instead of the opaque "non_loopback_or_bad_origin" surfaced at the call site. Verified against tests/hermes_cli/test_web_server_host_header.py (all 11 upstream tests still pass) and hand-tested by opening the bb/gui Electron desktop dev build against the patched backend. * fix(tui_gateway): restore _content_display_text helper Bb/gui had dropped the helper but the orchestrator code merged from main still calls it (_inflight_text, _message_preview). Re-add the definition verbatim from main so session.create / _start_inflight_turn don't crash with NameError on first prompt submit. * fix(tui-gateway): restore _content_display_text helper lost in main merge The May 27 merge of origin/main into bb/gui re-introduced two callers of _content_display_text (in _inflight_text and _history_to_messages) but dropped the helper definition itself, leaving an unresolved reference. NameError fires on every user message via _start_inflight_turn -> _inflight_text, taking down both the TUI and the desktop (which share this gateway backend) the moment input is dispatched. Restores the helper verbatim from main (commit 36c99af37) -- pure structured-content text extractor, no other dependencies. * fix(telegram): import Set for _dm_topic_chat_ids annotation self._dm_topic_chat_ids: Set[str] = {...} at line 460 references Set but only Dict, List, Optional, Any are imported from typing. The file has no 'from __future__ import annotations', so the annotation is evaluated at runtime and raises NameError on TelegramAdapter construction. * fix(setup): drop shadowing inner importlib.util re-imports _print_setup_summary and _setup_tts_provider each had 'import importlib.util' inside a try: block nested deeper in the function body. Python flips importlib to function-local for the whole scope, so earlier references in the same function (the neutts branches at lines 493 / 1109) hit UnboundLocalError before the late import can run. The top-of-module 'import importlib.util' at line 14 already covers both call sites, so dropping the redundant inner imports restores the intended behavior. * feat(install.ps1): add -IncludeDesktop switch + Stage-Desktop The new Hermes-Setup.exe (Tauri bootstrap installer) passes -IncludeDesktop so users who install via the GUI end up with a launchable Hermes.exe at apps/desktop/release/<os>-unpacked/. Existing flows are unchanged: * The 'irm install.ps1 | iex' CLI one-liner omits the flag — terminal users don't need a prebuilt desktop binary; 'hermes desktop' builds on demand. * The Electron desktop's bootstrap-runner.cjs also omits the flag — rebuilding apps/desktop from inside a running Hermes.exe would try to overwrite the live binary on disk and fail. Stage-Desktop runs after Stage-NodeDeps so workspace npm is already installed when electron-builder fires. It does: 1. 'npm install' at repo root so apps/* workspaces resolve their deps (Electron itself arrives via npm here, ~150MB) 2. 'npm run pack' in apps/desktop (tsc + vite + electron-builder --dir) 3. Probes apps/desktop/release/{win-unpacked,win-arm64-unpacked}/Hermes.exe The --dir mode produces an unpacked launchable binary without an NSIS/MSI installer artifact — we don't need one because Hermes-Setup.exe spawns the unpacked binary directly via launch_hermes_desktop. * feat(installer): Tauri bootstrap installer for first-time onboarding Hermes-Setup.exe is a small signed Rust+Tauri binary that drives scripts/install.ps1 stage-by-stage with a native UI matching the desktop's design language. Replaces the chicken-and-egg pattern of shipping a 200MB Electron app whose first launch existed only to run install.ps1. The architecture: Rust backend (src-tauri/): bootstrap.rs orchestrator -- Tauri commands, stage iteration install_script.rs resolve install.ps1 (dev checkout, cache, GitHub raw) powershell.rs spawn powershell, line-stream stdout/stderr, parse JSON events.rs BootstrapEvent types -- mirror bootstrap-runner.cjs paths.rs HERMES_HOME resolution + tracing log setup build.rs bakes BUILD_PIN_COMMIT / BUILD_PIN_BRANCH from 'git rev-parse HEAD' at compile time React frontend (src/): Tauri webview rendering 4 screens (welcome / progress / success / failure), driven by nanostores subscribing to the Rust event stream. Visual layer reuses the desktop's styles.css wholesale via @import so the installer and desktop never drift visually. Distribution: targets = ['app', 'dmg', 'appimage'] -- no NSIS/MSI wrapper. The raw target/release/Hermes-Setup.exe IS the artifact on Windows; .dmg + .app on macOS; AppImage on Linux. One file, double-click, no installer-installing-an-installer pattern. Compile-time pinning: build.rs reads 'git rev-parse HEAD' and emits cargo:rustc-env=BUILD_PIN_COMMIT=<sha> + BUILD_PIN_BRANCH=<branch>. bootstrap.rs's option_env!() picks these up so the binary fetches install.ps1 from the exact SHA it was tested against. CI / release builds can override via HERMES_BUILD_PIN_COMMIT env var. Windows manifest: hermes-setup.manifest declares level='asInvoker' so the productName 'Hermes Setup' doesn't trip Windows's installer- detection heuristic and refuse to launch without elevation. Also declares PerMonitorV2 DPI + UTF-8 active code page + Common Controls v6. Limitations of this initial version: * No code signing -- Windows SmartScreen will warn once on Hermes-Setup.exe ('More info -> Run anyway'). The downstream binaries it produces (Hermes.exe in win-unpacked/, the hermes CLI) are locally-built and therefore don't carry MOTW, so they launch without SmartScreen intervention. Cert procurement tracked separately. * macOS and Linux build paths defined but untested -- Windows-only V1. * fix(installer): pass -IncludeDesktop to manifest, surface launch errors, alias hermes desktop Three bugs found in the first VM end-to-end test: 1. install.ps1 -Manifest was called WITHOUT -IncludeDesktop, so the manifest came back with the 14-stage list (no desktop stage), the UI showed '14 steps' and Stage-Desktop never ran. Pass the flag to both the manifest fetch and the per-stage runs — install.ps1 gates the desktop stage's inclusion on the flag. 2. The Success screen's Launch button silently swallowed the Tauri error when no Hermes.exe existed (e.g. Stage-Desktop was skipped). Wire the error through to inline UI with an alert callout, so the user gets actionable text ('Hermes.exe missing, run hermes desktop from a terminal') instead of an unresponsive button. 3. The Success screen tells users to run 'hermes desktop' from a terminal but the CLI only accepted 'hermes gui' — invalid choice for 'desktop'. Rename the subcommand canonically to 'desktop' with 'gui' as a backwards-compatible alias. Update the _SUBCOMMANDS sets used by session-flag arg parsing + logging-mode probe so both names route to the same logic. * fix(install.ps1): pre-warm electron-builder winCodeSign cache + fix Stage-Desktop $HasNode false-skip Two bugs caught in the second VM end-to-end run: 1. electron-builder's winCodeSign extraction fails on grandma-class Windows boxes because the .7z archive contains macOS symlinks (darwin/10.12/lib/libcrypto.dylib and libssl.dylib pointing at versioned siblings). Creating symlinks on Windows requires SeCreateSymbolicLinkPrivilege, a per-user right that non-admin accounts don't have on stock Windows. Result: every fresh install on a non-admin user fails Stage-Desktop with a 7-Zip 'cannot create symbolic link' error, retried four times, then bails. Fix: Initialize-ElectronBuilderCache pre-extracts winCodeSign-2.6.0.7z ourselves with -snl (don't preserve symlinks, store as resolved file content) AND -x!darwin (skip the entire macOS subtree — irrelevant on Windows). Writes to electron-builder's expected cache dir before electron-builder gets a chance to try its own broken extraction. Idempotent — fast-paths via signtool.exe sentinel check. 2. Install-Desktop's first guard was 'if (-not $HasNode) skip'. $HasNode is set by Stage-Node into $script:HasNode, but in cross-process driver mode (each -Stage NAME is a fresh powershell.exe spawned by Hermes-Setup.exe), that script-scope variable from the PREVIOUS process is invisible — so the guard always fired and Install-Desktop returned in 900ms with a misleading 'Node.js not available' reason. The real npm probe below it never got to run. Fix: re-probe npm directly via Get-Command when $HasNode is empty/false, since by that point Stage-Node has already verified Node is installed and the only question is whether *this* process can see it on PATH (it can — installer-wide PATH update from Stage-Node). * fix(install.ps1): tell electron-builder we're NOT signing instead of pre-extracting winCodeSign The previous commit (c7e46f9f3) worked around the winCodeSign-symlinks- on-Windows extraction crash by pre-extracting the archive ourselves with -snl + -x!darwin. That fix was correct but addressed the wrong layer. The deeper question: why was electron-builder fetching winCodeSign at all when we have no signing cert configured? Answer: electron-builder unconditionally pre-warms the toolchain assuming any build MIGHT sign. The cert auto-discovery never finds anything (we never set CSC_LINK or anything else), so the signing never happens — but the 100MB fetch of winCodeSign and its broken-on-Windows symlink extraction does. Set CSC_IDENTITY_AUTO_DISCOVERY=false (with WIN_CSC_LINK and WIN_CSC_KEY_PASSWORD also explicitly cleared as belt-and-suspenders) before invoking npm run pack, and electron-builder skips the entire winCodeSign apparatus. No download, no extraction, no privilege check. Env vars are saved/restored around the invocation so we don't leak the override into Stage-PlatformSdks etc. Net: removes the 100-line Initialize-ElectronBuilderCache helper that manually downloaded + extracted winCodeSign-2.6.0.7z. Replaced with 3 env-var assignments. The produced Hermes.exe is functionally identical — just no longer carries a code-signing-machinery dependency we never used. * fix(installer): bump bootstrap-installer.log to capture stage transitions + every install.ps1 line Diagnosing the second VM failure was impossible because bootstrap-installer.log contained only the 'starting' banner. Two causes: 1. emit_log() inside run_bootstrap() was tracing::debug! — dropped on the floor under the default INFO env-filter. 2. The per-stage sink callbacks (on_stdout_line / on_stderr_line) only emitted Tauri events to the frontend; they never tee'd to the log file at all. When the failure route mounts, the Tauri event stream is the only place the script output lived, and it gets discarded. 3. The Failed / Stage / Manifest / Complete lifecycle frames in emit_event() were also Tauri-only — so even the 'which stage failed' frame never reached the log. Fixes: * emit_log() → tracing::info! * Sink callbacks tee stdout to info!, stderr to warn!, with stage label as a structured field for grep'ability * emit_event() now matches on the variant and logs each lifecycle frame at the right level: Failed → tracing::error!, others → info! Result: a failing install leaves a complete forensic trail in bootstrap-installer.log — manifest stage list, every install.ps1 stdout/stderr line tagged by stage, the stage transitions, and the final error. Same path as before so nothing the user does changes. * fix(install.ps1): Stage-NodeDeps cross-process $HasNode + stream npm install output to bootstrap log VM run 3 diagnosis: node-deps stage skipped on the VM (logged 'Skipping Node.js dependencies (Node not installed)') and then desktop's npm install failed with exit 1 and zero diagnostic detail. Two root causes: 1. $HasNode false-skip in Stage-NodeDeps — same cross-process bug pattern we fixed for Stage-Desktop in c7e46f9f3. Stage-Node ran in process A and set $script:HasNode = $true, then exited. Stage- NodeDeps ran in fresh process B (Hermes-Setup.exe -Stage NAME spawns each stage independently), where that variable doesn't exist. Re-probe via Get-Command npm instead of trusting the stale script-scope global. The previous stage already verified Node so the re-probe succeeds. 2. npm install --silent + Tee to TEMP file hid the real error. When the workspace install failed on the VM, the actual reason was buffered in $env:TEMP\hermes-npm-desktop-install-*.log and the user saw only 'exit 1'. Drop --silent so npm streams its full output, drop the TEMP-file dance — the Tauri installer's streaming sink already tees every stdout/stderr line to the rolling bootstrap-installer.log, so a side log file is dead weight that hides the very error we need. After this, the bootstrap log on a failure will contain npm's full output (deprecation warnings, ETARGET, native-module compile errors, whatever) tagged with stage=desktop, making the actual cause diagnosable instead of an opaque exit code. * fix(install.ps1): restore Initialize-ElectronBuilderCache (CSC env vars alone aren't enough) VM run 4 diagnosis: even with CSC_IDENTITY_AUTO_DISCOVERY=false set, electron-builder still fetches winCodeSign and signs bundled binaries. The log shows the signing happens BEFORE the cache extraction: • signing with signtool.exe ...\winpty-agent.exe • signing with signtool.exe ...\OpenConsole.exe • downloading winCodeSign-2.6.0.7z • <symlink privilege error> Cause: node-pty's bundled prebuilds are listed in apps/desktop's asarUnpack ['**/*.node', '**/prebuilds/**']. electron-builder re-signs anything unpacked from asar, regardless of whether OUR binary gets signed. The signtool invocation needs winCodeSign on disk, which needs the .7z extracted, which hits the macOS-symlink crash on non-admin Windows. The CSC env vars I added in d5fe46727 only kill IDENTITY DISCOVERY (so OUR Hermes.exe stays unsigned, which is fine — we have no cert). They don't prevent the toolchain fetch for the bundled-prebuild re-sign. I removed the pre-extract in d5fe46727 thinking the env vars subsumed it; that was wrong. Both are needed. Restoring Initialize-ElectronBuilderCache verbatim from c7e46f9f3 and keeping the CSC env vars. Wrote a clearer doc-comment at the call site explaining the two-knob interaction so future maintainers don't drop one half again. * fix(desktop): disable signtool via signtoolOptions.sign=null, drop dead winCodeSign pre-extract VM run 5 diagnosis: the pre-extract from 3b29e65c1 ran (extracted 83 files, 24MB) but produced ZERO files at the expected sentinel path '/winCodeSign-2.6.0/windows-10/x64/signtool.exe'. Cause: the .7z archive's root entries are 'windows-10/', 'darwin/', 'linux/', etc. — not 'winCodeSign-2.6.0/<arch>'. Extracting with '-o$cacheRoot' put files at $cacheRoot/windows-10/..., NOT at $cacheRoot/winCodeSign-2.6.0/windows-10/.... I had the directory nesting wrong from the start. And then we observed: electron-builder downloads winCodeSign-2.6.0.7z under a random numeric filename ('384387955.7z') regardless of what's already extracted in the parent dir. The cache key isn't the dirname; it's content-addressed. So the pre-extract approach was doomed even if the path nesting had been right. Actual fix: signtoolOptions.sign=null in apps/desktop/package.json's win build config. electron-builder honors this and skips the bundled- prebuild signing entirely — no signtool invocation, no winCodeSign fetch, no symlink-privilege crash. The previous failures all stemmed from electron-builder pre-signing node-pty's bundled .exes (winpty-agent.exe, OpenConsole.exe) which are already author-signed upstream; re-signing with our nonexistent cert was overwriting good sigs with nothing useful anyway. Cost: when we DO get a real cert later, we'll add it back with the sign function pointing at the cert chain. Until then, all-null is the correct config and unblocks every non-admin Windows user. Removed Initialize-ElectronBuilderCache (the dead pre-extract). Removed the call site. Kept the CSC_IDENTITY_AUTO_DISCOVERY env vars as belt-and-suspenders against a future electron-builder change that might revive cert auto-discovery. * fix(desktop): use no-op sign function instead of sign=null VM run 6 still hit the symlink crash even with signtoolOptions.sign=null. electron-builder 26.8.1 treats null as 'use the default signtool path' rather than 'skip signing', so the winCodeSign fetch + extraction still fired for the bundled prebuild re-sign. The Electron docs (electronjs.org/docs/latest/tutorial/code-signing) make it clear signing is OPTIONAL and unsigned apps work fine — users just see SmartScreen on first launch. The electron-builder mechanism for 'don't actually sign anything' is to supply a custom sign function (via signtoolOptions.sign: '<path-to-cjs-module>') that resolves without invoking signtool. build-noop-sign.cjs is that module — a 5-line async function that returns undefined. electron-builder calls it for every binary it would have signed, gets back a resolved promise, and considers each binary 'signed.' No signtool spawn, no winCodeSign fetch, no symlink crash. When Nous's cert arrives, replace this file with a real signing hook (@electron/windows-sign-based or a direct signtool invocation). The architecture's signing-ready and the cutover is a one-file edit. * fix(desktop): signAndEditExecutable=false to skip signtool path entirely After reading app-builder-lib/winPackager.js line 216 + 231 directly: signAndEditExecutable is the ACTUAL hardcoded gate that short-circuits both signApp() (which signs Hermes.exe + every shouldSignFile match including bundled prebuilds) AND createTransformerForExtraFiles(). None of signtoolOptions.sign / sign:null / sign:<custom-fn> gate the winCodeSign download — that happens before they're consulted. What we lose: rcedit also runs through signAndEditResources, so disabling this drops PE metadata (file properties showing 'Hermes' / 'Nous Research' / file description). Cost is real but bounded: * Hermes.exe filename, icon, asar contents, app identity intact * Task Manager shows 'Hermes.exe' (the filename) not 'Hermes' (PE description) — minor downgrade * Start menu, taskbar, window title all work normally * SmartScreen will warn once (unsigned, same as before) When the cert lands, flip signAndEditExecutable back to default true, both signing AND rcedit return, PE metadata is restored. Removes the no-op sign function (build-noop-sign.cjs) since signAndEditExecutable=false prevents signtool from being invoked at all — the custom hook never gets called either. * feat(install.ps1): write .hermes-bootstrap-complete marker at end of install The desktop app's main.cjs resolver ladder has a 'bootstrap-needed' rung that fires when .hermes-bootstrap-complete is missing from ACTIVE_HERMES_ROOT. Pre-Hermes-Setup, this marker was written by the packaged-desktop's own bootstrap-runner.cjs at the end of its install flow. Now that Hermes-Setup.exe runs install.ps1 directly, install.ps1 needs to own the marker — otherwise the desktop sees no marker on first launch and triggers its legacy first-launch bootstrap (re-running install.ps1 from inside Electron, the exact recursion Hermes-Setup.exe was supposed to obviate). Implementation: * New Stage-BootstrapMarker (worker) → Write-BootstrapMarker (helper) * Slotted in the manifest right after platform-sdks, before the interactive configure/gateway stages, so it runs unconditionally when the install reaches the finalize phase * Schema mirrors apps/desktop/electron/main.cjs writeBootstrapMarker / isBootstrapComplete EXACTLY: {schemaVersion: 1, pinnedCommit, pinnedBranch, completedAt}. Schema version stays at 1 so old desktops that read marker files written by future install.ps1s can still parse them. * pinnedCommit comes from -Commit flag (Hermes-Setup.exe passes it) or falls back to 'git rev-parse HEAD' in InstallDir * pinnedBranch from -Branch flag, defaults to 'main' matching install.ps1's own param default Two PS-5.1 gotchas baked into comments: * The ?. null-conditional operator doesn't exist pre-PS7; use explicit if-checks on Get-Command results * Set-Content -Encoding UTF8 emits a BOM in 5.1 and Node's plain JSON.parse rejects BOM — write via .NET's UTF8Encoding(false) to produce BOM-less JSON the desktop's readJson() can parse * feat(installer): drive in-app updates through the Tauri installer Converge update on the same principle as bootstrap: one driver owns all repo mutation. The desktop becomes a pure consumer that hands off to Hermes-Setup.exe --update instead of re-implementing git/pip in Electron. - hermes desktop --build-only: build without launching, so the installer owns the post-update launch (CLI keeps build logic single-sourced). - Installer AppMode {Install,Update} from argv; get_mode exposed to the UI. - Installer self-copies to HERMES_HOME/hermes-setup.exe on install success (no-op guard during --update re-invocation to avoid the locked-exe copy). - Installer --update flow (update.rs): wait for the desktop to release the venv shim, run 'hermes update --yes --gateway' (branch on exit 0/2/other), then 'hermes desktop --build-only', then launch the rebuilt desktop. Reuses the bootstrap event channel + progress UI via a synthetic two-stage manifest. - Desktop applyUpdates() gutted (~105 lines of git/stash/pull/pyproject/pip removed) -> thin handoff: spawn updater, app.quit() to free the shim. Detection (checkUpdates, commit changelog, behind-count) kept intact. - install.ps1 creates Start Menu + Desktop shortcuts to the packed Hermes.exe (never bare 'hermes desktop', which would rebuild every launch). * test update * fix(installer): pass --branch to hermes update in the --update flow The install is a detached-HEAD checkout of a pinned commit. Without --branch, 'hermes update' fell back to its default (main) and switched the checkout to main — a divergent branch that lacks the desktop CLI command — so the update targeted the wrong branch and the rebuild stage failed with 'invalid choice: desktop'. Thread BUILD_PIN_BRANCH (the branch this installer was built against, and the same branch the desktop detected the update on) into 'hermes update --branch <b>' so update + rebuild stay on-branch. * test update * fix(installer): stamp Hermes icon onto Hermes.exe via rcedit (no winCodeSign) The unpacked Hermes.exe showed the stock Electron icon + name in the taskbar because build.win.signAndEditExecutable=false disables BOTH electron-builder's signing AND its rcedit metadata/icon stamping. That flag is load-bearing: enabling it re-triggers signtool -> winCodeSign, whose macOS symlinks crash 7-Zip on non-admin Windows (unfixable dead end). Decouple identity-stamping from signing entirely: after npm run pack, run rcedit ourselves on the produced exe. - Add rcedit as a direct devDependency of apps/desktop (the transitive electron-winstaller copy is fragile). - apps/desktop/scripts/set-exe-identity.cjs: Node helper that calls rcedit's named export to set icon + ProductName/FileDescription/ CompanyName. Node builds argv natively — avoids the PowerShell->exe ->JSON double-escaping that broke the app-builder rcedit path. - install.ps1 Set-DesktopExeIdentity invokes the script after the build, before shortcuts. Best-effort: failure keeps the stock icon, never fails the install. rcedit is a pure PE editor — no signtool, no winCodeSign, no symlinks. Verified locally: stamping a copy of the built Hermes.exe embeds the 32x32 icon and sets ProductName=Hermes. Also fix update-path success-screen flash: in update mode the installer hands off + exits in ~600ms, so don't route to the 'launch Hermes' success view (it flashed before the window closed). * update test * fix(desktop): show 'hermes update' guidance for CLI installs instead of dead-end error A user who installed via the CLI (irm|iex / install.sh) then ran `hermes desktop` has no staged hermes-setup.exe, so clicking Update in-app hit resolveUpdaterBinary()=null and showed a misleading error ('re-run the Hermes installer') with a Try-again button that could never succeed — a dead loop for a perfectly valid install. Treat the no-updater case as an intentional outcome, not a failure: - main.cjs applyUpdates returns { ok:true, manual:true, command:'hermes update' } (no throw, no 'error' stage) when no updater binary exists. - New 'manual' update stage + apply-state.command thread the command to the UI. - updates-overlay ManualView: a polished terminal-native card with the exact command and a copy button, framed as the correct path for a CLI user rather than an error. GUI-installer users are unaffected — hermes-setup.exe present => seamless auto-update runs as before. Zero new process orchestration; can't fail the update demo. * update test * fix(gui): pin /api/hermes/update to the current branch The desktop command-center 'update' action hits POST /api/hermes/update, which spawned bare `hermes update` with no --branch. cmd_update then falls back to its default (main) and checks the working tree OUT of the tracked branch — a bb/gui install silently jumped to main and lost the desktop CLI. Resolve the checkout's current branch and pass --branch <current> from this endpoint only. The engine default (main) is DELIBERATELY unchanged: bare `hermes update` from a terminal, the gateway /update bot command, and the CLI/TUI relaunch path all keep their long-standing 'update against main' contract for the existing user base. Only the GUI button is scoped to update-the-branch-you're-on. Detached HEAD / git failure falls back to the bare default. * update test * fix(desktop): branch-pin the CLI manual-update command card The 'Update from your terminal' card (shown to CLI installs with no staged updater) hardcoded bare `hermes update` — which defaults to main and would switch a bb/gui (or any non-main) checkout off-branch. Same bug we fixed for the GUI button, leaked into the card's copy text. Resolve the checkout's current branch and show `hermes update --branch <current>` for non-main checkouts; keep it bare for main so the card stays clean. Best-effort: bare fallback if branch detection fails. Matches the GUI button + installer --update contract; bare terminal/bot/TUI update paths still default to main, unchanged. * docs: phragg was here * feat(desktop): lead onboarding with Nous Portal + fix fresh-install detection (#34970) - Feature Nous Portal as the primary onboarding card (Recommended tag, app logo, single pitch line); collapse other OAuth providers behind an "Other providers" disclosure whose open/closed state persists. - Surface OpenRouter as a one-click API-key option inside the disclosure; move "I have an API key" to a quiet bottom-right link. - Treat "no provider configured" as a normal onboarding state, not a red error banner (provider-setup-errors copy match). - Fix setup.runtime_check: it reported ready when the resolved runtime had an empty credential or only implicit Bedrock/IAM, so fresh installs never saw onboarding. Now requires a usable credential. - Auto-wire Windows fonts for WSL2 users so the renderer renders real Segoe UI instead of the DejaVu fallback; make WSL detection env-independent via the /proc kernel marker. * feat(desktop): live elapsed timer on install bootstrap steps The first-launch install overlay showed a static "Installing" with no motion, so long steps (notably the repo clone) looked frozen. Stamp each stage's start time on the running transition and tick once a second so the active step shows live elapsed (e.g. "Installing · 1:23"), plus elapsed on the overall current-step line. Completed steps keep their final duration. * fix(desktop): resolve PortableGit for update checks + reserve titlebar tools space - runGit() hardcoded spawn('git'), which ENOENTs on fresh installer-driven Windows installs (git is PortableGit under %LOCALAPPDATA%\hermes\git, never on PATH) — so "Check for updates" failed with "Couldn't check for updates". Add resolveGitBinary() mirroring findGitBash (PortableGit → Git-for-Windows → PATH) and use it in runGit. - PageSearchShell rendered a full-width search input in the titlebar row, so on Windows its right edge slid under the fixed top-right tools + native window controls. Reserve that footprint via --titlebar-tools-* vars. * fix(desktop): stop streaming caret from shifting layout on completion The streaming caret (::after on the running message's last child) was an in-flow inline-block adding ~0.78em of inline width, which could wrap the last line mid-stream; when the caret is removed on completion the line un-wraps and reflows — the visible post-response layout shift. Net-zero its inline advance with a compensating negative margin so it paints at the text end without consuming layout width. * fix(desktop): stop completed-message layout shift while streaming The assistant message action bar used `hideWhenRunning`, which unmounts it whenever the thread is streaming. Since the bar reserves vertical space in each completed assistant message's footer (it's invisible-until-hover via opacity, not via mount), unmounting it collapsed every prior turn by the bar's height — then remounting on resolve grew them back, shifting the whole conversation (visible as "padding appears above the last user message"). Drop hideWhenRunning so the footer height is constant; the bar stays invisible during streaming via its existing opacity/pointer-events gating. * fix(merge): keep windows-footgun suppressions inline * fix(merge): keep remaining gateway footgun suppressions inline * fix(merge): restore contracts caught by main-target CI * fix(dashboard): honor injected HERMES_DASHBOARD_SESSION_TOKEN The desktop shell mints a session token and signs its /api + /api/ws calls with it via HERMES_DASHBOARD_SESSION_TOKEN, but the main-merge restored a web_server.py that ignored the env var and minted its own random _SESSION_TOKEN -- so every desktop request 401'd and the UI reported "gateway offline". Read the injected token (fall back to a fresh random one) so loopback HTTP + WS auth line up. Adds a regression test so a future merge can't silently drop the read. * fix(desktop): align fresh-install home so upgraders don't brick Two related first-launch bugs on machines with a legacy ~/.hermes: - install.ps1 hardcoded $HermesHome/$InstallDir to %LOCALAPPDATA%\hermes and ignored the HERMES_HOME the desktop passes through. The desktop freezes HERMES_HOME at module load and prefers a legacy ~/.hermes when %LOCALAPPDATA%\hermes is absent, so the installer wrote to a different home than the shell read -> "Could not connect to Hermes gateway". Honor $env:HERMES_HOME in the param defaults. - isBootstrapComplete() trusted the marker + checkout without verifying a runnable venv, so an interrupted/split install spawned a dead backend instead of re-bootstrapping. Also require the venv python to exist. * fix(dashboard): allow packaged desktop file:// origin on loopback WS The packaged Electron desktop loads its renderer over file://, so its /api/ws handshake carries Origin: file:// (or null). The DNS-rebinding WebSocket Origin guard only accepted http(s) origins matching the bound host, so it rejected the desktop's own renderer with 4403 -> "Could not connect to Hermes gateway" on macOS. A browser DNS-rebinding attacker can only ever present an http(s) origin (the site hosting the malicious page); it cannot forge file://, null, or a custom app scheme AND hold the loopback session token. So on loopback binds we now trust non-web origins -- the token in _ws_auth_ok remains the real authenticator. Public/gated binds still reject them, and cross-site http(s) origins are still rejected everywhere. * fix(desktop): resolve renderer assets relative to BASE_URL Absolute public asset paths (/apple-touch-icon.png, /ds-assets/...) work under the dev server but break in the packaged app, where the renderer is loaded from file://.../index.html and a leading slash resolves to the filesystem root -> broken onboarding provider icon and backdrop image on macOS. Prefix these with import.meta.env.BASE_URL so they resolve next to the bundled index.html in both dev and packaged builds. * feat(desktop): automate first-launch bootstrap on macOS/Linux Previously a packaged macOS/Linux app with no Hermes install hit a dead-end ("first-launch install is not yet automated -- run install.sh manually") because install.sh lacked the staged protocol install.ps1 exposes. Now both platforms bootstrap on first launch with the same structured, per-step progress UI as Windows. - install.sh: add --manifest / --stage / --json / --non-interactive plus a stage dispatcher (prerequisites, repository, venv, python-deps, node-deps, path, config, setup, gateway, complete). User-input stages (setup, gateway) are skipped under --non-interactive; the in-app onboarding overlay owns API keys/model, matching the Windows flow. Each stage runs inside the install dir (its own process) and a new --commit flag pins the checkout to the build-stamp SHA. - bootstrap-runner.cjs: drive the staged manifest/stage/JSON protocol for both install.ps1 (PowerShell) and install.sh (bash), selected by installer kind; removed the single-blob POSIX shim. - main.cjs: drop the macOS/Linux unsupported-platform dead-end so the bootstrap-needed path runs the installer on every platform. * fix(dashboard): return 404 JSON for unmatched /api paths instead of SPA HTML The SPA catch-all (serve_spa) served index.html for any unmatched GET, including unregistered /api/* endpoints. A missing API route therefore came back as <!doctype html> with status 200, and JSON clients (the desktop app's fetchJson) crashed with an opaque 'SyntaxError: Unexpected token <' instead of a clear error. - web_server.py: unmatched /api or /api/... now returns 404 JSON ('No such API endpoint'); non-api paths still serve the SPA for client-side routing. - main.cjs fetchJson: detect an HTML body / text/html content-type on a 2xx response and reject with a clear message naming the URL, rather than a raw JSON.parse SyntaxError. Empty bodies resolve to null; malformed JSON reports the URL plus a snippet. * say 'OS appearance' instead of 'macOS appearance' * feat(install): add --include-desktop stage + PowerShell-style flags to install.sh Brings install.sh to parity with install.ps1's bootstrap surface so the shared Rust/Tauri bootstrapper (apps/bootstrap-installer) can drive a macOS/Linux install the same way it drives Windows. - Accept the PowerShell-style aliases the bootstrapper emits to both installers: -Commit / -Branch (alongside existing -Manifest / -Stage / -Json / -NonInteractive). - Add --include-desktop / -IncludeDesktop. When set, the manifest gains a 'desktop' stage (immediately before 'complete'), and a new install_desktop runs a root workspace `npm install` + `npm run pack` (electron-builder --dir, signing auto-discovery disabled) to produce release/mac*/Hermes.app -- mirroring install.ps1's Install-Desktop / Stage-Desktop. - The flag is opt-in, exactly like Windows: the signed bootstrap installer passes it; the Electron app's own first-launch bootstrap and the CLI one-liner omit it (building the desktop from inside the running app would clobber it). * fix: tts endpoints * macOS desktop: install + in-app self-update (#35607) * fix(installer): align macOS HERMES_HOME with the rest of the stack paths.rs computed the macOS Hermes home as ~/Library/Application Support/ hermes, but nothing else does: hermes_constants.get_hermes_home() (Python), scripts/install.sh, and the Electron desktop's resolveHermesHome() all use ~/.hermes on macOS. The drift meant the Tauri installer wrote the install to one directory and the desktop looked for it in another, so a fresh GUI install never found its backend (the file's own comment warned this exact drift would break things). Use ~/.hermes on macOS to match. * fix(install.sh): always emit a stage result frame on failure Stage helpers (clone_repo, install_deps, check_python, …) were written for the monolithic flow and call `exit 1` on failure. Under `--stage`, that terminated the process before the JSON result frame was printed, so the installer's parse_stage_result saw "no frame" instead of a clean {ok:false,...} contract response. Run the stage body in a subshell so an `exit` only unwinds the subshell and the parent still emits the frame. * feat(install.sh): auto-provision git on macOS/Linux (parity with install.ps1) install.ps1 downloads PortableGit on Windows, but install.sh just printed a "please install git" hint and exited — so a fresh Mac with no developer tools (no Xcode CLT → no git) couldn't get past the clone step. check_git now tries to install git before bailing: - macOS: Homebrew if present (headless), else `xcode-select --install` (the CLT prompt also provides the compiler some wheels need), polling for git to appear. - Linux: apt/dnf/pacman via sudo when available. Falls back to the manual instructions only if auto-provision fails. * feat(desktop): in-app GUI+backend self-update on macOS/Linux On Windows the staged Hermes-Setup binary drives updates (quit → hermes update → hermes desktop --build-only → relaunch). The mac drag-install has no such binary, so "Update now" previously just printed `hermes update`. Since there's no venv-shim file lock on POSIX, the desktop can drive the whole update itself. applyUpdates now, when no staged updater exists on mac/linux: 1. runs `hermes update --yes [--branch <current>]` (backend git pull + deps), 2. runs `hermes desktop --build-only` (OS-aware GUI rebuild) with the Hermes-managed Node + venv on PATH, 3. spawns a detached swapper that waits for this process to exit, dittos the freshly built Hermes.app over the running bundle, clears quarantine, and relaunches. Degrades to "backend updated — restart to load the new GUI" if the rebuild fails or there's no .app bundle to swap (dev run, Linux AppImage). * chore: uptick * chore: uptick * chore: linux build * fix(install): detect xcode-select git stub on fresh macOS * chore: bump * fix(desktop): repair voice dictation on Windows Voice dictation was broken on Windows in two ways: 1. Mic access was denied. The Electron permission request handler only granted 'media' requests whose details.mediaTypes included 'audio', but Chromium on Windows frequently fires the mic request with an empty mediaTypes array, so getUserMedia threw NotAllowedError. The handler now grants audio-capture when mediaTypes includes 'audio' OR is empty/absent, handles the 'audioCapture' permission name, and adds a setPermissionCheckHandler (the synchronous path Chromium also consults for getUserMedia on Windows). Video is still denied. 2. Transcripts went nowhere. The composer's insertText handler (used by dictation and other inserts) only updated the assistant-ui composer store via setText, never the contentEditable editor DOM. The draft->editor sync effect only re-renders the editor when it is NOT focused, and dictation runs while the editor has/regains focus, so the transcript was stored but never shown and could not be sent. insertText now renders into the editor DOM and places the caret, mirroring appendExternalText. Also hardens fetchJson: a 2xx response with an HTML body (or text/html content-type) now rejects with a clear message naming the URL instead of an opaque JSON.parse 'Unexpected token <' error. * feat(desktop): route Nous subscribers onto the Tool Gateway from the GUI When the GUI sets the main provider to Nous via POST /api/model/set, call the same apply_nous_managed_defaults the CLI uses after model selection, so GUI/onboarding users land on the Nous Tool Gateway the same way CLI users do — no separate prompt, no duplicated logic. Purely additive: apply_nous_managed_defaults skips any tool where the user has a direct key (FIRECRAWL_API_KEY, FAL_KEY, etc.) or explicit config, so it never overwrites a user's own setup. Only unconfigured tools get routed. - web_server.py: in set_model_assignment (scope=main, provider=nous), resolve enabled toolsets and apply managed defaults; guarded so a Portal hiccup never blocks saving the model. Returns routed tools as gateway_tools. - onboarding.ts: surface a 'Tool Gateway enabled' toast listing routed tools. - types/hermes.ts: add gateway_tools to ModelAssignmentResponse. - tests: cover nous-applies, non-nous-skips, and failure-doesnt-block-save. * feat(desktop): mirror hermes model free/paid curation in GUI onboarding GUI onboarding picked models[0] from /api/model/options, which ignores the Nous free/paid tier — a free user could land on a paid default (e.g. anthropic/claude-opus-4). Now the recommended default mirrors what `hermes model` does. - web_server.py: new GET /api/model/recommended-default?provider=<slug>. For Nous it runs the same curation as the CLI (get_curated_nous_model_ids + pricing + check_nous_free_tier + union_with_portal_{free,paid}_recommendations + partition_nous_models_by_tier) so free users get a free model and paid users get the curated default. Other providers fall back to the first curated model. Never 500s — returns empty model on error so onboarding degrades gracefully. - hermes.ts: getRecommendedDefaultModel client + RecommendedDefaultModel type. - onboarding.ts: fetchProviderDefaultModel prefers the recommended endpoint, falls back to models[0] when unavailable. - tests: free-tier picks free model, paid-tier picks curated default, failure returns empty without 500. * feat(desktop): show model pricing + free/paid tier gating in GUI picker The CLI `hermes model` picker shows per-model $/Mtok pricing and gates paid models on free Nous accounts. The GUI picker showed bare model names. Bring it to parity across both the model-picker dialog and onboarding confirm card. Backend: - inventory.build_models_payload gains a pricing=True flag → _apply_pricing enriches each provider row with formatted per-model pricing ({input,output,cache,free}) via the same _format_price_per_mtok the CLI uses, and for Nous adds free_tier + unavailable_models (paid models a free user can't select) via check_nous_free_tier + partition_nous_models_by_tier. Best-effort: any pricing/tier failure is swallowed and fails open (no gating). - /api/model/options and TUI model.options now pass pricing=True so the global picker and in-session picker both carry pricing. Frontend: - ModelOptionProvider gains pricing/free_tier/unavailable_models; new ModelPricing type. - model-picker dialog renders In/Out $/Mtok (or a Free pill) per model, a Free tier/Pro badge on the Nous heading, and disables + grays unavailable paid models for free users with a 'Pro models need a paid subscription' note. - onboarding confirm card shows the chosen model's price + tier badge. Tests: test_inventory_pricing covers price formatting, free-tier gating, paid no-gating, providers without pricing, and swallowed failures. * fix(desktop): GUI model picker shows curated Nous list in curated order Two bugs made the GUI Nous model list diverge from the `hermes model` CLI picker: 1. Backend (model_switch.py): the Nous row in list_authenticated_providers fell through to cached_provider_model_ids("nous"), dumping the full live /v1/models catalog (~50 vendor-prefixed models, alphabetical). Now it uses the curated list AND applies the Portal free/paid recommendation union — exactly like _model_flow_nous in main.py — so newly-launched models such as stepfun/step-3.7-flash:free surface in curated order. Best-effort: falls back to the curated list alone if the Portal fetch fails. 2. Frontend (model-picker.tsx): cmdk's Command had shouldFilter on (default), which re-sorts items by fuzzy-match score (≈alphabetical) and ignores array order. Set shouldFilter={false} + own the search term and do an order-preserving substring filter, so the backend's curated order is shown verbatim. * feat(desktop): add/switch providers from the model picker via onboarding reuse The model picker could only select models from already-authenticated providers. Switching to a new provider had no in-app path. Rather than duplicate provider UI, reuse the existing onboarding provider selector (featured Nous + other providers + API-key form + device-code/PKCE flow + model-confirm with pricing/tier). - onboarding store: add a 'manual' flag with startManualOnboarding() / closeManualOnboarding(). Manual mode forces the onboarding overlay to show even when configured===true and refreshOnboarding no longer auto-dismisses on runtime-ready (the app is already working — the user is just adding or switching a provider). - onboarding overlay: render when manual even if configured; show a Close button (the first-run flow has none since the app can't run yet). - model picker: 'Add provider' footer button opens the onboarding selector; ModelResults lists only configured (model-bearing) providers. * feat(desktop): add PUT /api/tools/toolsets/{name} enable/disable endpoint * feat(desktop): add toggleToolset RPC binding * feat(desktop): toolset enable/disable switch in Tools settings * feat(desktop): tool configuration parity in GUI Tools settings Bring the desktop GUI Tools settings to parity with the CLI `hermes tools` for provider selection and API-key configuration. Backend (hermes_cli/web_server.py): - GET /api/tools/toolsets/{name}/config - provider matrix + key status - PUT /api/tools/toolsets/{name}/provider - persist provider selection Shared core (hermes_cli/tools_config.py): - Extract apply_provider_selection / _write_provider_config from the interactive _configure_provider so the CLI and GUI write identical config keys (web.backend, tts.provider, browser.cloud_provider, plugin image/video providers, use_gateway flags) through one code path. Desktop UI: - ToolsetConfigPanel: provider list with select, per-provider API-key entry (set/replace/clear/reveal via the shared env RPCs), Ready/Needs keys state, guidance for Nous-auth and post-setup providers. - Wire the Configured/Needs keys pill to expand the panel inline; refresh the toolset list after key changes so the pill updates live. - Add getToolsetConfig / selectToolsetProvider RPC bindings + types. Post-setup (OAuth/install) flows still defer to the CLI; see docs spike findings for the planned /api/tools/setup/* endpoint family. Tests: backend round-trip + 400 cases for the new endpoints and apply_provider_selection; desktop vitest coverage for the config panel (provider render, select, key save). No change-detector tests. Also removes three stale completed plan docs. * fix(desktop): show real Hermes version + sync package.json on release The desktop app version was disconnected from the Hermes version: the release script bumped pyproject.toml + hermes_cli/__init__.py but never touched apps/desktop/package.json, which sat stale at 0.0.2 (lockfile at 0.0.1). - main.cjs: hermes:version IPC now resolves __version__ from hermes_cli/__init__.py (the canonical source release.py bumps) via a new resolveHermesVersion() helper, falling back to app.getVersion() when the source tree isn't readable. The About panel now always shows the live Hermes version and can't drift. - release.py: update_version_files() also bumps apps/desktop/package.json in lockstep with pyproject (top-level version only; dep specs untouched). - One-time catch-up: package.json 0.0.2 -> 0.15.1 and the lockfile root mirrors 0.0.1 -> 0.15.1. * fix(desktop): stamp exe identity in afterPack hook so updates stay branded The packed Hermes.exe reverted to the stock Electron icon + "Electron" name after an in-app update. The icon/identity stamp (rcedit) lived only in install.ps1, but the installer's --update path rebuilds the desktop via `hermes desktop --build-only` -> `npm run pack`, which never ran install.ps1 and so never stamped the rebuilt exe. Move the stamp into an electron-builder afterPack hook so it runs for EVERY packed build regardless of caller (first install, hermes desktop, the update rebuild, or a manual npm run pack): - set-exe-identity.cjs: refactor to export stampExeIdentity(exe, desktopRoot); still runnable as a standalone CLI. - after-pack.cjs (new): afterPack hook calling stampExeIdentity. Windows-only guard; best-effort (logs + resolves on failure, never fails the build). - package.json: register build.afterPack. - install.ps1: remove the now-redundant Set-DesktopExeIdentity function + call; the hook handles it during npm run pack. electron-builder's own rcedit step stays disabled (signAndEditExecutable=false) to avoid the signtool -> winCodeSign -> 7-Zip macOS-symlink crash on non-admin Windows; the hook runs rcedit directly (pure PE resource edit, no signing). * fix(desktop): export afterPack hook as exports.default so electron-builder runs it The afterPack hook used `module.exports = fn`, which electron-builder's hook loader doesn't pick up — it expects the function as the module's default export (the same shape afterSign/notarize.cjs uses). The hook silently never ran, so even first install shipped the stock "Electron" exe. Switch to `exports.default = async function afterPack(...)`. Verified with a real `npm run pack`: electron-builder now invokes the hook and the produced release/win-unpacked/Hermes.exe carries ProductName/FileDescription=Hermes. * chore(desktop): drop auto-build release CI in favor of manual build + upload Remove desktop-release.yml (nightly-on-main + stable publish). Installers are now built locally per platform and uploaded to a GitHub Release by hand; the website points at them via NEXT_PUBLIC_HERMES_DL_* env. Update README + docs and drop the dead desktop-nightly channel links. * fix(desktop): stable shortcut icon + bust icon cache so updates repaint Symptom on a freshly-installed laptop: Hermes.exe itself shows the correct Hermes icon (Explorer reads the live exe's stamped PE resource), but the desktop shortcut still draws the stock Electron icon. Cause: New-DesktopShortcuts set IconLocation to "<exe>,0", so Windows cached the icon it extracted from the exe at shortcut-creation time. On an update the exe gets re-stamped, but the shortcut keeps rendering the stale cached bitmap. - package.json: ship assets/icon.ico beside the exe via extraResources (-> resources/icon.ico). Verified with a real npm run pack. - install.ps1 New-DesktopShortcuts: point IconLocation at resources/icon.ico (fallback to <exe>,0 if absent) — a dedicated .ico is cache-stable and skips the per-exe extraction that goes stale. Then run `ie4uinit.exe -show` to bust the shell icon cache so the shortcut repaints immediately instead of showing the old Electron icon until reboot. Both best-effort; never fail an otherwise-good install. * dummy update * feat(desktop): self-heal update branch + backend contract guard Two fixes for the bb/gui→main transition: - Self-update self-heals: if the tracked branch (e.g. bb/gui) no longer exists on origin (merged + deleted), the desktop updater falls back to main and persists it. Read-only ls-remote probe that only flips on a definitive "ref absent" (exit 2), never on a transient network error, so already-installed clients migrate themselves with no manual flip. - Backend contract guard: tui_gateway reports DESKTOP_BACKEND_CONTRACT in session runtime info; the desktop warns with a one-click "Update Hermes" when the backend predates the GUI's required contract (e.g. a bb/gui app pointed at a main checkout) instead of failing cryptically downstream. * docs(desktop): rewrite README to match current install/update/build flow The old README contradicted itself (claimed a bundled Python payload while also saying it no longer bundles source) and predated cross-platform support. Rewrite for accuracy: Linux is a first-class build target, install.sh/install.ps1 both drive the staged bootstrap, the real self-update handoff (Windows Hermes-Setup vs in-app macOS/Linux), and the bb/gui→main self-heal + backend contract guard. * docs(desktop): rewrite README as a real product readme Lead with what the app is and how to get it (download an installer, or `hermes desktop` for existing CLI users) plus a plain-language feature list, then keep contributor/build/internals as a clearly separated secondary section. * docs(desktop): fix install framing — releases no longer auto-build installers Lead with the install-with-Hermes path (`--include-desktop` / `hermes desktop`), which always works, and describe prebuilt installers as manually published when a release ships them rather than implying CI attaches them to every release. * docs(desktop): match base repo README style Adopt the root README's conventions: centered title + badge row, bold one-liner intro, a feature <table> grid, --- section dividers, and a Community / License footer. * feat(desktop): recover from gateway boot failures + validate API keys on entry (#35864) Fresh installs that hit a gateway boot failure had no recovery path: the shell rendered dead ("gateway offline"), logs were undiscoverable, and a mistyped API key was accepted because onboarding only checked credential presence, not validity. - Add BootFailureOverlay: a top-level recovery surface (Retry, Repair install, Use local gateway, Open logs + inline recent logs) that mounts on any hard boot failure, including post-install. Trims the now-redundant recovery button from the onboarding Preparing panel. - Add hermes:logs:reveal / :recent IPC (reveal desktop.log) and a hermes:bootstrap:repair IPC that drops the bootstrap marker to force a clean reinstall. Surface "Open logs" in Gateway settings too. - Add POST /api/providers/validate: a live per-provider probe (OpenRouter/OpenAI/xAI/Gemini key check, local endpoint connectivity) wired into saveOnboardingApiKey so a rejected key blocks before it's persisted, while an unreachable probe falls through (offline-safe). * test(model-catalog): fix stale nous picker test after curated-list change ac2e48907 made the GUI/picker Nous row use the curated list (curated["nous"] = get_curated_nous_model_ids()) + Portal union, matching the `hermes model` CLI — but test_picker_nous_row_uses_manifest still asserted the old 2-model manifest snapshot, breaking the test shard. Rewrite it as an invariant: stub the Portal union to passthrough and assert the row equals get_curated_nous_model_ids() computed under the same conditions, so it tracks the real contract instead of a hardcoded model list that rots on every catalog update. --------- Co-authored-by: emozilla <emozilla@nousresearch.com> Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> Co-authored-by: Austin Pickett <pickett.austin@gmail.com> Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: ethernet <arilotter@gmail.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
2026-05-31 17:46:56 -05:00
# Keep the desktop Electron app's package.json version in lockstep with the
# Python package version. The desktop About panel reads the live Hermes
# version at runtime, but app.getVersion()/packaging metadata still come
# from this field, so it must track pyproject to avoid drift.
desktop_pkg = REPO_ROOT / "apps" / "desktop" / "package.json"
if desktop_pkg.exists():
pkg_text = desktop_pkg.read_text(encoding="utf-8")
pkg_text = re.sub(
r'("version"\s*:\s*)"[^"]+"',
rf'\g<1>"{semver}"',
pkg_text,
count=1,
)
desktop_pkg.write_text(pkg_text, encoding="utf-8")
# Update ACP Registry manifest + npm launcher (must stay version-locked
# with pyproject — enforced by tests/acp/test_registry_manifest.py).
_update_acp_registry_versions(semver)
def _update_acp_registry_versions(semver: str) -> None:
"""Bump the ACP Registry manifest's version + uvx package pin in lockstep
with pyproject.
Skips silently if the manifest is missing older release branches predate
the ACP Registry assets.
"""
if ACP_REGISTRY_MANIFEST.exists():
manifest = json.loads(ACP_REGISTRY_MANIFEST.read_text(encoding="utf-8"))
manifest["version"] = semver
uvx = manifest.get("distribution", {}).get("uvx", {})
if "package" in uvx:
uvx["package"] = f"hermes-agent[acp]=={semver}"
# Preserve trailing newline + 2-space indent the file already uses.
ACP_REGISTRY_MANIFEST.write_text(
json.dumps(manifest, indent=2) + "\n", encoding="utf-8"
)
def build_release_artifacts(semver: str) -> list[Path]:
"""Build sdist/wheel artifacts for the current release.
ci: add PyPI publish workflow (salvaged from #25901) (#26148) * ci(pypi): add publish workflow for automated PyPI releases Triggered by CalVer tag pushes from scripts/release.py (v20* pattern). Three jobs: build (uv build) → publish (OIDC trusted publishing) → sign (Sigstore + attach to existing GitHub Release). - workflow_dispatch as manual escape hatch - skip-existing for safe re-runs - Graceful skip when GitHub Release not found (sign job) - Top-level permissions: contents: read (CodeQL compliant) Requires one-time setup: PyPI trusted publisher + GitHub pypi environment. Co-authored-by: dmahan93 <44207705+dmahan93@users.noreply.github.com> * fix(release): address review findings - Stage acp_registry/agent.json in version bump commit (was silently left unstaged) - Add missing return when no previous tags found without --first-release - Fix get_pr_number return type annotation (str -> str | None) - Prefer uv build over python -m build (matches CI workflow), with fallback - Use unit separator (%x1f) in git log format to handle | in author names - Add explicit encoding='utf-8' to .release_notes.md write Workflow hardening: - Gracefully skip signing when GitHub Release not found (env var gate instead of exit 1, so PyPI publish still shows green) * fix(ci): harden PyPI workflow — SHA-pin actions, guard workflow_dispatch, explicit build flags - Pin all actions to commit SHAs (supply-chain hardening for id-token:write) - workflow_dispatch now requires confirm_tag input + checks out that tag - Both uv build paths explicitly pass --sdist --wheel --------- Co-authored-by: dmahan93 <44207705+dmahan93@users.noreply.github.com>
2026-05-15 13:21:48 +05:30
Tries ``uv build`` first (matching the CI workflow), falls back to
``python -m build`` if uv is unavailable.
"""
dist_dir = REPO_ROOT / "dist"
shutil.rmtree(dist_dir, ignore_errors=True)
ci: add PyPI publish workflow (salvaged from #25901) (#26148) * ci(pypi): add publish workflow for automated PyPI releases Triggered by CalVer tag pushes from scripts/release.py (v20* pattern). Three jobs: build (uv build) → publish (OIDC trusted publishing) → sign (Sigstore + attach to existing GitHub Release). - workflow_dispatch as manual escape hatch - skip-existing for safe re-runs - Graceful skip when GitHub Release not found (sign job) - Top-level permissions: contents: read (CodeQL compliant) Requires one-time setup: PyPI trusted publisher + GitHub pypi environment. Co-authored-by: dmahan93 <44207705+dmahan93@users.noreply.github.com> * fix(release): address review findings - Stage acp_registry/agent.json in version bump commit (was silently left unstaged) - Add missing return when no previous tags found without --first-release - Fix get_pr_number return type annotation (str -> str | None) - Prefer uv build over python -m build (matches CI workflow), with fallback - Use unit separator (%x1f) in git log format to handle | in author names - Add explicit encoding='utf-8' to .release_notes.md write Workflow hardening: - Gracefully skip signing when GitHub Release not found (env var gate instead of exit 1, so PyPI publish still shows green) * fix(ci): harden PyPI workflow — SHA-pin actions, guard workflow_dispatch, explicit build flags - Pin all actions to commit SHAs (supply-chain hardening for id-token:write) - workflow_dispatch now requires confirm_tag input + checks out that tag - Both uv build paths explicitly pass --sdist --wheel --------- Co-authored-by: dmahan93 <44207705+dmahan93@users.noreply.github.com>
2026-05-15 13:21:48 +05:30
# Prefer uv build (matches CI workflow), fall back to python -m build.
uv_bin = shutil.which("uv")
if uv_bin:
cmd = [uv_bin, "build", "--sdist", "--wheel"]
else:
cmd = [sys.executable, "-m", "build", "--sdist", "--wheel"]
result = subprocess.run(
ci: add PyPI publish workflow (salvaged from #25901) (#26148) * ci(pypi): add publish workflow for automated PyPI releases Triggered by CalVer tag pushes from scripts/release.py (v20* pattern). Three jobs: build (uv build) → publish (OIDC trusted publishing) → sign (Sigstore + attach to existing GitHub Release). - workflow_dispatch as manual escape hatch - skip-existing for safe re-runs - Graceful skip when GitHub Release not found (sign job) - Top-level permissions: contents: read (CodeQL compliant) Requires one-time setup: PyPI trusted publisher + GitHub pypi environment. Co-authored-by: dmahan93 <44207705+dmahan93@users.noreply.github.com> * fix(release): address review findings - Stage acp_registry/agent.json in version bump commit (was silently left unstaged) - Add missing return when no previous tags found without --first-release - Fix get_pr_number return type annotation (str -> str | None) - Prefer uv build over python -m build (matches CI workflow), with fallback - Use unit separator (%x1f) in git log format to handle | in author names - Add explicit encoding='utf-8' to .release_notes.md write Workflow hardening: - Gracefully skip signing when GitHub Release not found (env var gate instead of exit 1, so PyPI publish still shows green) * fix(ci): harden PyPI workflow — SHA-pin actions, guard workflow_dispatch, explicit build flags - Pin all actions to commit SHAs (supply-chain hardening for id-token:write) - workflow_dispatch now requires confirm_tag input + checks out that tag - Both uv build paths explicitly pass --sdist --wheel --------- Co-authored-by: dmahan93 <44207705+dmahan93@users.noreply.github.com>
2026-05-15 13:21:48 +05:30
cmd,
cwd=str(REPO_ROOT),
capture_output=True,
text=True,
)
if result.returncode != 0:
print(" ⚠ Could not build Python release artifacts.")
stderr = result.stderr.strip()
stdout = result.stdout.strip()
if stderr:
print(f" {stderr.splitlines()[-1]}")
elif stdout:
print(f" {stdout.splitlines()[-1]}")
ci: add PyPI publish workflow (salvaged from #25901) (#26148) * ci(pypi): add publish workflow for automated PyPI releases Triggered by CalVer tag pushes from scripts/release.py (v20* pattern). Three jobs: build (uv build) → publish (OIDC trusted publishing) → sign (Sigstore + attach to existing GitHub Release). - workflow_dispatch as manual escape hatch - skip-existing for safe re-runs - Graceful skip when GitHub Release not found (sign job) - Top-level permissions: contents: read (CodeQL compliant) Requires one-time setup: PyPI trusted publisher + GitHub pypi environment. Co-authored-by: dmahan93 <44207705+dmahan93@users.noreply.github.com> * fix(release): address review findings - Stage acp_registry/agent.json in version bump commit (was silently left unstaged) - Add missing return when no previous tags found without --first-release - Fix get_pr_number return type annotation (str -> str | None) - Prefer uv build over python -m build (matches CI workflow), with fallback - Use unit separator (%x1f) in git log format to handle | in author names - Add explicit encoding='utf-8' to .release_notes.md write Workflow hardening: - Gracefully skip signing when GitHub Release not found (env var gate instead of exit 1, so PyPI publish still shows green) * fix(ci): harden PyPI workflow — SHA-pin actions, guard workflow_dispatch, explicit build flags - Pin all actions to commit SHAs (supply-chain hardening for id-token:write) - workflow_dispatch now requires confirm_tag input + checks out that tag - Both uv build paths explicitly pass --sdist --wheel --------- Co-authored-by: dmahan93 <44207705+dmahan93@users.noreply.github.com>
2026-05-15 13:21:48 +05:30
print(" Install uv or the 'build' package to attach sdist/wheel assets.")
return []
artifacts = sorted(p for p in dist_dir.iterdir() if p.is_file())
matching = [p for p in artifacts if semver in p.name]
if not matching:
print(" ⚠ Built artifacts did not match the expected release version.")
return []
return matching
def resolve_author(name: str, email: str) -> str:
"""Resolve a git author to a GitHub @mention."""
# Try email lookup first
gh_user = AUTHOR_MAP.get(email)
if gh_user:
return f"@{gh_user}"
# Try noreply pattern
noreply_match = re.match(r"(\d+)\+(.+)@users\.noreply\.github\.com", email)
if noreply_match:
return f"@{noreply_match.group(2)}"
# Try username@users.noreply.github.com
noreply_match2 = re.match(r"(.+)@users\.noreply\.github\.com", email)
if noreply_match2:
return f"@{noreply_match2.group(1)}"
# Fallback to git name
return name
def categorize_commit(subject: str) -> str:
"""Categorize a commit by its conventional commit prefix."""
subject_lower = subject.lower()
# Match conventional commit patterns
patterns = {
"breaking": [r"^breaking[\s:(]", r"^!:", r"BREAKING CHANGE"],
"features": [r"^feat[\s:(]", r"^feature[\s:(]", r"^add[\s:(]"],
"fixes": [r"^fix[\s:(]", r"^bugfix[\s:(]", r"^bug[\s:(]", r"^hotfix[\s:(]"],
"improvements": [r"^improve[\s:(]", r"^perf[\s:(]", r"^enhance[\s:(]",
r"^refactor[\s:(]", r"^cleanup[\s:(]", r"^clean[\s:(]",
r"^update[\s:(]", r"^optimize[\s:(]"],
"docs": [r"^doc[\s:(]", r"^docs[\s:(]"],
"tests": [r"^test[\s:(]", r"^tests[\s:(]"],
"chore": [r"^chore[\s:(]", r"^ci[\s:(]", r"^build[\s:(]",
r"^deps[\s:(]", r"^bump[\s:(]"],
}
for category, regexes in patterns.items():
for regex in regexes:
if re.match(regex, subject_lower):
return category
# Heuristic fallbacks
if any(w in subject_lower for w in ["add ", "new ", "implement", "support "]):
return "features"
if any(w in subject_lower for w in ["fix ", "fixed ", "resolve", "patch "]):
return "fixes"
if any(w in subject_lower for w in ["refactor", "cleanup", "improve", "update "]):
return "improvements"
return "other"
def clean_subject(subject: str) -> str:
"""Clean up a commit subject for display."""
# Remove conventional commit prefix
cleaned = re.sub(r"^(feat|fix|docs|chore|refactor|test|perf|ci|build|improve|add|update|cleanup|hotfix|breaking|enhance|optimize|bugfix|bug|feature|tests|deps|bump)[\s:(!]+\s*", "", subject, flags=re.IGNORECASE)
# Remove trailing issue refs that are redundant with PR links
cleaned = cleaned.strip()
# Capitalize first letter
if cleaned:
cleaned = cleaned[0].upper() + cleaned[1:]
return cleaned
def parse_coauthors(body: str) -> list:
"""Extract Co-authored-by trailers from a commit message body.
Returns a list of {'name': ..., 'email': ...} dicts.
Filters out AI assistants and bots (Claude, Copilot, Cursor, etc.).
"""
if not body:
return []
# AI/bot emails to ignore in co-author trailers
_ignored_emails = {"noreply@anthropic.com", "noreply@github.com",
"cursoragent@cursor.com", "hermes@nousresearch.com"}
_ignored_names = re.compile(r"^(Claude|Copilot|Cursor Agent|GitHub Actions?|dependabot|renovate)", re.IGNORECASE)
pattern = re.compile(r"Co-authored-by:\s*(.+?)\s*<([^>]+)>", re.IGNORECASE)
results = []
for m in pattern.finditer(body):
name, email = m.group(1).strip(), m.group(2).strip()
if email in _ignored_emails or _ignored_names.match(name):
continue
results.append({"name": name, "email": email})
return results
def get_commits(since_tag=None):
"""Get commits since a tag (or all commits if None)."""
if since_tag:
range_spec = f"{since_tag}..HEAD"
else:
range_spec = "HEAD"
ci: add PyPI publish workflow (salvaged from #25901) (#26148) * ci(pypi): add publish workflow for automated PyPI releases Triggered by CalVer tag pushes from scripts/release.py (v20* pattern). Three jobs: build (uv build) → publish (OIDC trusted publishing) → sign (Sigstore + attach to existing GitHub Release). - workflow_dispatch as manual escape hatch - skip-existing for safe re-runs - Graceful skip when GitHub Release not found (sign job) - Top-level permissions: contents: read (CodeQL compliant) Requires one-time setup: PyPI trusted publisher + GitHub pypi environment. Co-authored-by: dmahan93 <44207705+dmahan93@users.noreply.github.com> * fix(release): address review findings - Stage acp_registry/agent.json in version bump commit (was silently left unstaged) - Add missing return when no previous tags found without --first-release - Fix get_pr_number return type annotation (str -> str | None) - Prefer uv build over python -m build (matches CI workflow), with fallback - Use unit separator (%x1f) in git log format to handle | in author names - Add explicit encoding='utf-8' to .release_notes.md write Workflow hardening: - Gracefully skip signing when GitHub Release not found (env var gate instead of exit 1, so PyPI publish still shows green) * fix(ci): harden PyPI workflow — SHA-pin actions, guard workflow_dispatch, explicit build flags - Pin all actions to commit SHAs (supply-chain hardening for id-token:write) - workflow_dispatch now requires confirm_tag input + checks out that tag - Both uv build paths explicitly pass --sdist --wheel --------- Co-authored-by: dmahan93 <44207705+dmahan93@users.noreply.github.com>
2026-05-15 13:21:48 +05:30
# Format: hash<US>author_name<US>author_email<US>subject\0body
# Using %x1f (unit separator) to avoid conflict with | in author names
log = git(
"log", range_spec,
ci: add PyPI publish workflow (salvaged from #25901) (#26148) * ci(pypi): add publish workflow for automated PyPI releases Triggered by CalVer tag pushes from scripts/release.py (v20* pattern). Three jobs: build (uv build) → publish (OIDC trusted publishing) → sign (Sigstore + attach to existing GitHub Release). - workflow_dispatch as manual escape hatch - skip-existing for safe re-runs - Graceful skip when GitHub Release not found (sign job) - Top-level permissions: contents: read (CodeQL compliant) Requires one-time setup: PyPI trusted publisher + GitHub pypi environment. Co-authored-by: dmahan93 <44207705+dmahan93@users.noreply.github.com> * fix(release): address review findings - Stage acp_registry/agent.json in version bump commit (was silently left unstaged) - Add missing return when no previous tags found without --first-release - Fix get_pr_number return type annotation (str -> str | None) - Prefer uv build over python -m build (matches CI workflow), with fallback - Use unit separator (%x1f) in git log format to handle | in author names - Add explicit encoding='utf-8' to .release_notes.md write Workflow hardening: - Gracefully skip signing when GitHub Release not found (env var gate instead of exit 1, so PyPI publish still shows green) * fix(ci): harden PyPI workflow — SHA-pin actions, guard workflow_dispatch, explicit build flags - Pin all actions to commit SHAs (supply-chain hardening for id-token:write) - workflow_dispatch now requires confirm_tag input + checks out that tag - Both uv build paths explicitly pass --sdist --wheel --------- Co-authored-by: dmahan93 <44207705+dmahan93@users.noreply.github.com>
2026-05-15 13:21:48 +05:30
"--format=%H%x1f%an%x1f%ae%x1f%s%x00%b%x00",
"--no-merges",
)
if not log:
return []
commits = []
# Split on double-null to get each commit entry, since body ends with \0
# and format ends with \0, each record ends with \0\0 between entries
for entry in log.split("\0\0"):
entry = entry.strip()
if not entry:
continue
ci: add PyPI publish workflow (salvaged from #25901) (#26148) * ci(pypi): add publish workflow for automated PyPI releases Triggered by CalVer tag pushes from scripts/release.py (v20* pattern). Three jobs: build (uv build) → publish (OIDC trusted publishing) → sign (Sigstore + attach to existing GitHub Release). - workflow_dispatch as manual escape hatch - skip-existing for safe re-runs - Graceful skip when GitHub Release not found (sign job) - Top-level permissions: contents: read (CodeQL compliant) Requires one-time setup: PyPI trusted publisher + GitHub pypi environment. Co-authored-by: dmahan93 <44207705+dmahan93@users.noreply.github.com> * fix(release): address review findings - Stage acp_registry/agent.json in version bump commit (was silently left unstaged) - Add missing return when no previous tags found without --first-release - Fix get_pr_number return type annotation (str -> str | None) - Prefer uv build over python -m build (matches CI workflow), with fallback - Use unit separator (%x1f) in git log format to handle | in author names - Add explicit encoding='utf-8' to .release_notes.md write Workflow hardening: - Gracefully skip signing when GitHub Release not found (env var gate instead of exit 1, so PyPI publish still shows green) * fix(ci): harden PyPI workflow — SHA-pin actions, guard workflow_dispatch, explicit build flags - Pin all actions to commit SHAs (supply-chain hardening for id-token:write) - workflow_dispatch now requires confirm_tag input + checks out that tag - Both uv build paths explicitly pass --sdist --wheel --------- Co-authored-by: dmahan93 <44207705+dmahan93@users.noreply.github.com>
2026-05-15 13:21:48 +05:30
# Split on first null to separate "hash<US>name<US>email<US>subject" from "body"
if "\0" in entry:
header, body = entry.split("\0", 1)
body = body.strip()
else:
header = entry
body = ""
ci: add PyPI publish workflow (salvaged from #25901) (#26148) * ci(pypi): add publish workflow for automated PyPI releases Triggered by CalVer tag pushes from scripts/release.py (v20* pattern). Three jobs: build (uv build) → publish (OIDC trusted publishing) → sign (Sigstore + attach to existing GitHub Release). - workflow_dispatch as manual escape hatch - skip-existing for safe re-runs - Graceful skip when GitHub Release not found (sign job) - Top-level permissions: contents: read (CodeQL compliant) Requires one-time setup: PyPI trusted publisher + GitHub pypi environment. Co-authored-by: dmahan93 <44207705+dmahan93@users.noreply.github.com> * fix(release): address review findings - Stage acp_registry/agent.json in version bump commit (was silently left unstaged) - Add missing return when no previous tags found without --first-release - Fix get_pr_number return type annotation (str -> str | None) - Prefer uv build over python -m build (matches CI workflow), with fallback - Use unit separator (%x1f) in git log format to handle | in author names - Add explicit encoding='utf-8' to .release_notes.md write Workflow hardening: - Gracefully skip signing when GitHub Release not found (env var gate instead of exit 1, so PyPI publish still shows green) * fix(ci): harden PyPI workflow — SHA-pin actions, guard workflow_dispatch, explicit build flags - Pin all actions to commit SHAs (supply-chain hardening for id-token:write) - workflow_dispatch now requires confirm_tag input + checks out that tag - Both uv build paths explicitly pass --sdist --wheel --------- Co-authored-by: dmahan93 <44207705+dmahan93@users.noreply.github.com>
2026-05-15 13:21:48 +05:30
parts = header.split("\x1f", 3)
if len(parts) != 4:
continue
sha, name, email, subject = parts
coauthor_info = parse_coauthors(body)
coauthors = [resolve_author(ca["name"], ca["email"]) for ca in coauthor_info]
commits.append({
"sha": sha,
"short_sha": sha[:8],
"author_name": name,
"author_email": email,
"subject": subject,
"category": categorize_commit(subject),
"github_author": resolve_author(name, email),
"coauthors": coauthors,
})
return commits
ci: add PyPI publish workflow (salvaged from #25901) (#26148) * ci(pypi): add publish workflow for automated PyPI releases Triggered by CalVer tag pushes from scripts/release.py (v20* pattern). Three jobs: build (uv build) → publish (OIDC trusted publishing) → sign (Sigstore + attach to existing GitHub Release). - workflow_dispatch as manual escape hatch - skip-existing for safe re-runs - Graceful skip when GitHub Release not found (sign job) - Top-level permissions: contents: read (CodeQL compliant) Requires one-time setup: PyPI trusted publisher + GitHub pypi environment. Co-authored-by: dmahan93 <44207705+dmahan93@users.noreply.github.com> * fix(release): address review findings - Stage acp_registry/agent.json in version bump commit (was silently left unstaged) - Add missing return when no previous tags found without --first-release - Fix get_pr_number return type annotation (str -> str | None) - Prefer uv build over python -m build (matches CI workflow), with fallback - Use unit separator (%x1f) in git log format to handle | in author names - Add explicit encoding='utf-8' to .release_notes.md write Workflow hardening: - Gracefully skip signing when GitHub Release not found (env var gate instead of exit 1, so PyPI publish still shows green) * fix(ci): harden PyPI workflow — SHA-pin actions, guard workflow_dispatch, explicit build flags - Pin all actions to commit SHAs (supply-chain hardening for id-token:write) - workflow_dispatch now requires confirm_tag input + checks out that tag - Both uv build paths explicitly pass --sdist --wheel --------- Co-authored-by: dmahan93 <44207705+dmahan93@users.noreply.github.com>
2026-05-15 13:21:48 +05:30
def get_pr_number(subject: str) -> str | None:
"""Extract PR number from commit subject if present."""
match = re.search(r"#(\d+)", subject)
if match:
return match.group(1)
return None
def generate_changelog(commits, tag_name, semver, repo_url="https://github.com/NousResearch/hermes-agent",
prev_tag=None, first_release=False):
"""Generate markdown changelog from categorized commits."""
lines = []
# Header
now = datetime.now()
date_str = now.strftime("%B %d, %Y")
lines.append(f"# Hermes Agent v{semver} ({tag_name})")
lines.append("")
lines.append(f"**Release Date:** {date_str}")
lines.append("")
if first_release:
lines.append("> 🎉 **First official release!** This marks the beginning of regular weekly releases")
lines.append("> for Hermes Agent. See below for everything included in this initial release.")
lines.append("")
# Group commits by category
categories = defaultdict(list)
all_authors = set()
teknium_aliases = {"@teknium1"}
for commit in commits:
categories[commit["category"]].append(commit)
author = commit["github_author"]
if author not in teknium_aliases:
all_authors.add(author)
for coauthor in commit.get("coauthors", []):
if coauthor not in teknium_aliases:
all_authors.add(coauthor)
# Category display order and emoji
category_order = [
("breaking", "⚠️ Breaking Changes"),
("features", "✨ Features"),
("improvements", "🔧 Improvements"),
("fixes", "🐛 Bug Fixes"),
("docs", "📚 Documentation"),
("tests", "🧪 Tests"),
("chore", "🏗️ Infrastructure"),
("other", "📦 Other Changes"),
]
for cat_key, cat_title in category_order:
cat_commits = categories.get(cat_key, [])
if not cat_commits:
continue
lines.append(f"## {cat_title}")
lines.append("")
for commit in cat_commits:
subject = clean_subject(commit["subject"])
pr_num = get_pr_number(commit["subject"])
author = commit["github_author"]
# Build the line
parts = [f"- {subject}"]
if pr_num:
parts.append(f"([#{pr_num}]({repo_url}/pull/{pr_num}))")
else:
parts.append(f"([`{commit['short_sha']}`]({repo_url}/commit/{commit['sha']}))")
if author not in teknium_aliases:
parts.append(f"{author}")
lines.append(" ".join(parts))
lines.append("")
# Contributors section
if all_authors:
# Sort contributors by commit count
author_counts = defaultdict(int)
for commit in commits:
author = commit["github_author"]
if author not in teknium_aliases:
author_counts[author] += 1
for coauthor in commit.get("coauthors", []):
if coauthor not in teknium_aliases:
author_counts[coauthor] += 1
sorted_authors = sorted(author_counts.items(), key=lambda x: -x[1])
lines.append("## 👥 Contributors")
lines.append("")
lines.append("Thank you to everyone who contributed to this release!")
lines.append("")
for author, count in sorted_authors:
commit_word = "commit" if count == 1 else "commits"
lines.append(f"- {author} ({count} {commit_word})")
lines.append("")
# Full changelog link
if prev_tag:
lines.append(f"**Full Changelog**: [{prev_tag}...{tag_name}]({repo_url}/compare/{prev_tag}...{tag_name})")
else:
lines.append(f"**Full Changelog**: [{tag_name}]({repo_url}/commits/{tag_name})")
lines.append("")
return "\n".join(lines)
def main():
parser = argparse.ArgumentParser(description="Hermes Agent Release Tool")
parser.add_argument("--bump", choices=["major", "minor", "patch"],
help="Which semver component to bump")
parser.add_argument("--publish", action="store_true",
help="Actually create the tag and GitHub release (otherwise dry run)")
parser.add_argument("--date", type=str,
help="Override CalVer date (format: YYYY.M.D)")
parser.add_argument("--first-release", action="store_true",
help="Mark as first release (no previous tag expected)")
parser.add_argument("--output", type=str,
help="Write changelog to file instead of stdout")
args = parser.parse_args()
# Determine CalVer date
if args.date:
calver_date = args.date
else:
now = datetime.now()
calver_date = f"{now.year}.{now.month}.{now.day}"
base_tag = f"v{calver_date}"
tag_name, calver_date = next_available_tag(base_tag)
if tag_name != base_tag:
print(f"Note: Tag {base_tag} already exists, using {tag_name}")
# Determine semver
current_version = get_current_version()
if args.bump:
new_version = bump_version(current_version, args.bump)
else:
new_version = current_version
# Get previous tag
prev_tag = get_last_tag()
if not prev_tag and not args.first_release:
print("No previous tags found. Use --first-release for the initial release.")
print(f"Would create tag: {tag_name}")
print(f"Would set version: {new_version}")
ci: add PyPI publish workflow (salvaged from #25901) (#26148) * ci(pypi): add publish workflow for automated PyPI releases Triggered by CalVer tag pushes from scripts/release.py (v20* pattern). Three jobs: build (uv build) → publish (OIDC trusted publishing) → sign (Sigstore + attach to existing GitHub Release). - workflow_dispatch as manual escape hatch - skip-existing for safe re-runs - Graceful skip when GitHub Release not found (sign job) - Top-level permissions: contents: read (CodeQL compliant) Requires one-time setup: PyPI trusted publisher + GitHub pypi environment. Co-authored-by: dmahan93 <44207705+dmahan93@users.noreply.github.com> * fix(release): address review findings - Stage acp_registry/agent.json in version bump commit (was silently left unstaged) - Add missing return when no previous tags found without --first-release - Fix get_pr_number return type annotation (str -> str | None) - Prefer uv build over python -m build (matches CI workflow), with fallback - Use unit separator (%x1f) in git log format to handle | in author names - Add explicit encoding='utf-8' to .release_notes.md write Workflow hardening: - Gracefully skip signing when GitHub Release not found (env var gate instead of exit 1, so PyPI publish still shows green) * fix(ci): harden PyPI workflow — SHA-pin actions, guard workflow_dispatch, explicit build flags - Pin all actions to commit SHAs (supply-chain hardening for id-token:write) - workflow_dispatch now requires confirm_tag input + checks out that tag - Both uv build paths explicitly pass --sdist --wheel --------- Co-authored-by: dmahan93 <44207705+dmahan93@users.noreply.github.com>
2026-05-15 13:21:48 +05:30
return
# Get commits
commits = get_commits(since_tag=prev_tag)
if not commits:
print("No new commits since last tag.")
if not args.first_release:
return
print(f"{'='*60}")
print(f" Hermes Agent Release Preview")
print(f"{'='*60}")
print(f" CalVer tag: {tag_name}")
print(f" SemVer: v{current_version} → v{new_version}")
print(f" Previous tag: {prev_tag or '(none — first release)'}")
print(f" Commits: {len(commits)}")
print(f" Unique authors: {len({c['github_author'] for c in commits})}")
print(f" Mode: {'PUBLISH' if args.publish else 'DRY RUN'}")
print(f"{'='*60}")
print()
# Generate changelog
changelog = generate_changelog(
commits, tag_name, new_version,
prev_tag=prev_tag,
first_release=args.first_release,
)
if args.output:
codebase: add encoding='utf-8' to all bare open() calls (PLW1514) Closes the last Python-on-Windows UTF-8 exposure by making every text-mode open() call explicit about its encoding. Before: on Windows, bare open(path, 'r') defaults to the system locale encoding (cp1252 on US-locale installs). That means reading any config/yaml/markdown/json file with non-ASCII content either crashes with UnicodeDecodeError or silently mis-decodes bytes. After: all 89 affected call sites in production code now pass encoding='utf-8' explicitly. Works identically on every platform and every locale, no surprise behavior. Mechanical sweep via: ruff check --preview --extend-select PLW1514 --unsafe-fixes --fix --exclude 'tests,venv,.venv,node_modules,website,optional-skills, skills,tinker-atropos,plugins' . All 89 fixes have the same shape: open(x) or open(x, mode) became open(x, encoding='utf-8') or open(x, mode, encoding='utf-8'). Nothing else changed. Every modified file still parses and the Windows/sandbox test suite is still green (85 passed, 14 skipped, 0 failed across tests/tools/test_code_execution_windows_env.py + tests/tools/test_code_execution_modes.py + tests/tools/test_env_passthrough.py + tests/test_hermes_bootstrap.py). Scope notes: - tests/ excluded: test fixtures can use locale encoding intentionally (exercising edge cases). If we want to tighten tests later that's a separate PR. - plugins/ excluded: plugin-specific conventions may differ; plugin authors own their code. - optional-skills/ and skills/ excluded: skill scripts are user-authored and we don't want to mass-edit them. - website/ and tinker-atropos/ excluded: vendored / generated content. 46 files touched, 89 +/- lines (symmetric replacement). No behavior change on POSIX or on Windows when the file is ASCII; bug fix on Windows when the file contains non-ASCII.
2026-05-07 19:24:45 -07:00
Path(args.output).write_text(changelog, encoding="utf-8")
print(f"Changelog written to {args.output}")
else:
print(changelog)
if args.publish:
print(f"\n{'='*60}")
print(" Publishing release...")
print(f"{'='*60}")
# Update version files
if args.bump:
update_version_files(new_version, calver_date)
print(f" ✓ Updated version files to v{new_version} ({calver_date})")
# Commit version bump
ci: add PyPI publish workflow (salvaged from #25901) (#26148) * ci(pypi): add publish workflow for automated PyPI releases Triggered by CalVer tag pushes from scripts/release.py (v20* pattern). Three jobs: build (uv build) → publish (OIDC trusted publishing) → sign (Sigstore + attach to existing GitHub Release). - workflow_dispatch as manual escape hatch - skip-existing for safe re-runs - Graceful skip when GitHub Release not found (sign job) - Top-level permissions: contents: read (CodeQL compliant) Requires one-time setup: PyPI trusted publisher + GitHub pypi environment. Co-authored-by: dmahan93 <44207705+dmahan93@users.noreply.github.com> * fix(release): address review findings - Stage acp_registry/agent.json in version bump commit (was silently left unstaged) - Add missing return when no previous tags found without --first-release - Fix get_pr_number return type annotation (str -> str | None) - Prefer uv build over python -m build (matches CI workflow), with fallback - Use unit separator (%x1f) in git log format to handle | in author names - Add explicit encoding='utf-8' to .release_notes.md write Workflow hardening: - Gracefully skip signing when GitHub Release not found (env var gate instead of exit 1, so PyPI publish still shows green) * fix(ci): harden PyPI workflow — SHA-pin actions, guard workflow_dispatch, explicit build flags - Pin all actions to commit SHAs (supply-chain hardening for id-token:write) - workflow_dispatch now requires confirm_tag input + checks out that tag - Both uv build paths explicitly pass --sdist --wheel --------- Co-authored-by: dmahan93 <44207705+dmahan93@users.noreply.github.com>
2026-05-15 13:21:48 +05:30
add_files = [str(VERSION_FILE), str(PYPROJECT_FILE)]
if ACP_REGISTRY_MANIFEST.exists():
add_files.append(str(ACP_REGISTRY_MANIFEST))
add_result = git_result("add", *add_files)
if add_result.returncode != 0:
print(f" ✗ Failed to stage version files: {add_result.stderr.strip()}")
return
commit_result = git_result(
"commit", "-m", f"chore: bump version to v{new_version} ({calver_date})"
)
if commit_result.returncode != 0:
print(f" ✗ Failed to commit version bump: {commit_result.stderr.strip()}")
return
print(f" ✓ Committed version bump")
# Create annotated tag
tag_result = git_result(
"tag", "-a", tag_name, "-m",
f"Hermes Agent v{new_version} ({calver_date})\n\nWeekly release"
)
if tag_result.returncode != 0:
print(f" ✗ Failed to create tag {tag_name}: {tag_result.stderr.strip()}")
return
print(f" ✓ Created tag {tag_name}")
# Push
push_result = git_result("push", "origin", "HEAD", "--tags")
if push_result.returncode == 0:
print(f" ✓ Pushed to origin")
else:
print(f" ✗ Failed to push to origin: {push_result.stderr.strip()}")
print(" Continue manually after fixing access:")
print(" git push origin HEAD --tags")
# Build semver-named Python artifacts so downstream packagers
# (e.g. Homebrew) can target them without relying on CalVer tag names.
artifacts = build_release_artifacts(new_version)
if artifacts:
print(" ✓ Built release artifacts:")
for artifact in artifacts:
print(f" - {artifact.relative_to(REPO_ROOT)}")
# Create GitHub release
changelog_file = REPO_ROOT / ".release_notes.md"
ci: add PyPI publish workflow (salvaged from #25901) (#26148) * ci(pypi): add publish workflow for automated PyPI releases Triggered by CalVer tag pushes from scripts/release.py (v20* pattern). Three jobs: build (uv build) → publish (OIDC trusted publishing) → sign (Sigstore + attach to existing GitHub Release). - workflow_dispatch as manual escape hatch - skip-existing for safe re-runs - Graceful skip when GitHub Release not found (sign job) - Top-level permissions: contents: read (CodeQL compliant) Requires one-time setup: PyPI trusted publisher + GitHub pypi environment. Co-authored-by: dmahan93 <44207705+dmahan93@users.noreply.github.com> * fix(release): address review findings - Stage acp_registry/agent.json in version bump commit (was silently left unstaged) - Add missing return when no previous tags found without --first-release - Fix get_pr_number return type annotation (str -> str | None) - Prefer uv build over python -m build (matches CI workflow), with fallback - Use unit separator (%x1f) in git log format to handle | in author names - Add explicit encoding='utf-8' to .release_notes.md write Workflow hardening: - Gracefully skip signing when GitHub Release not found (env var gate instead of exit 1, so PyPI publish still shows green) * fix(ci): harden PyPI workflow — SHA-pin actions, guard workflow_dispatch, explicit build flags - Pin all actions to commit SHAs (supply-chain hardening for id-token:write) - workflow_dispatch now requires confirm_tag input + checks out that tag - Both uv build paths explicitly pass --sdist --wheel --------- Co-authored-by: dmahan93 <44207705+dmahan93@users.noreply.github.com>
2026-05-15 13:21:48 +05:30
changelog_file.write_text(changelog, encoding="utf-8")
gh_cmd = [
"gh", "release", "create", tag_name,
"--title", f"Hermes Agent v{new_version} ({calver_date})",
"--notes-file", str(changelog_file),
]
gh_cmd.extend(str(path) for path in artifacts)
gh_bin = shutil.which("gh")
if gh_bin:
result = subprocess.run(
gh_cmd,
capture_output=True, text=True,
cwd=str(REPO_ROOT),
)
else:
result = None
if result and result.returncode == 0:
changelog_file.unlink(missing_ok=True)
print(f" ✓ GitHub release created: {result.stdout.strip()}")
print(f"\n 🎉 Release v{new_version} ({tag_name}) published!")
else:
if result is None:
print(" ✗ GitHub release skipped: `gh` CLI not found.")
else:
print(f" ✗ GitHub release failed: {result.stderr.strip()}")
print(f" Release notes kept at: {changelog_file}")
print(f" Tag was created locally. Create the release manually:")
print(
f" gh release create {tag_name} --title 'Hermes Agent v{new_version} ({calver_date})' "
f"--notes-file .release_notes.md {' '.join(str(path) for path in artifacts)}"
)
print(f"\n ✓ Release artifacts prepared for manual publish: v{new_version} ({tag_name})")
else:
print(f"\n{'='*60}")
print(f" Dry run complete. To publish, add --publish")
print(f" Example: python scripts/release.py --bump minor --publish")
print(f"{'='*60}")
if __name__ == "__main__":
main()