upgrade: rebase fork customizations onto upstream e0b9ab5ac (2026-07-22) - #9
Merged
Conversation
Worktrees symlink node_modules to the main checkout; the dir-only node_modules/ pattern doesn't match symlinks, so one slipped into a commit and broke npm ci on CI (ENOTDIR). Dropping the trailing slash matches both.
…NousResearch#69019) content-visibility:auto on turn groups (perf: off-screen turns skip style/layout/paint) pairs with contain-intrinsic-size:auto, which only remembers a turn's size after it renders. A turn that finished streaming near the bottom had its smaller mid-stream size remembered; once it scrolled off the top edge and got skipped, it collapsed to that stale height. With overflow-anchor:none the viewport can't self-correct, so the stick-to-bottom lock drifts and the view creeps up over older turns — the 'long session eventually shows old responses' visual glitch. Exempt the newest turns (live tail) from virtualization so a turn is only ever skipped after its layout has settled at its final size (remembered == real -> skipping changes no height). Off-screen older turns still skip, so the dialog/popover whole-document recalc win on long transcripts is kept (it scales with the hundreds of old turns, not the small tail).
…d-hardening fix(ui-tui): widget-grid hardening — review fast-follow for NousResearch#20379
… apps The SDK the desktop app already has, ported to the TUI: a WidgetApp contract (id/help/mode/init/reduce/render/usage), a registry, and a host that owns the active widget, routes input to its reducer, and renders it. The grid-test and dialog-test debug surfaces are reimplemented as widget apps instead of bespoke overlay state, and slash commands are generated from the registry. Input for an open widget is owned by the active app (supersedes the demo-only stacked-modal routing) — the single active widget enforces topmost-owns-input structurally.
… ASCII art /weather [location]: wttr.in current conditions behind a Dialog, art bucket table-driven off WWO weather codes, every tint a theme family tone (sun = primary, rain = shell blue, thunder = warn). Proves the async story the demos don't: init returns a loading phase and fires the fetch; results land through the new host.updateWidget, which patches state ONLY while the app is still active — a late resolution can never resurrect a closed app or clobber a different one. `r` refetches; Esc/q/Enter close. Four async-contract tests (loading→ready via updateWidget, late-resolution guard, error phase, keymap). 1253 TS tests green.
…n-flow dock Widgets can render as ambient (glanceable, non-blocking) instead of modal, docked in the normal layout flow above/below the status bar rather than taking over the screen. The slash catalog is generated from the widget registry so new apps surface automatically, and /ticker lands as the first live-animation ambient demo.
…kill Hermes can write its own widgets: a loader discovers $HERMES_HOME/tui-widgets/*.mjs, fs.watch hot-loads them the moment they land (no restart), and a tui-widgets skill teaches the agent the contract and the openWidget-at-register auto-open recipe. Load/error/remove events announce themselves in the transcript; a lazy intro skeleton covers the first paint.
…streams Reusable render primitives the SDK exposes to widget authors: sparkline/gauge/ hbars chart helpers (dimension-stable so live updates never resize the card), an Accordion for expand/collapse sections, animated shimmer loaders, and a streams demo that no longer reserves a phantom icon column on unfocused titles.
A full placement grid so the agent can put a widget where it asks — dock-top/ bottom and corner zones, with corners as reserved rails that take real space instead of floating over content. A per-widget error boundary plus lenient ShimmerRows means generated widget code can't crash the TUI.
host.tsx collapses to one placement router over a shared render context, and the grid-test app drops its width floor too (carrying the NousResearch#20379 review rule). Final formatting pass folded in.
… desktop Make the Python skin engine the single source of truth for a canonical theme shape consumed by every surface, so a skin authored in $HERMES_HOME/skins/*.yaml (by a user or by Hermes from a prompt) themes the CLI, TUI, and desktop GUI at once — the theme analogue of the plugin SDK. - @hermes/shared: canonical `HermesSkin` token shape + `SKIN_COLOR_TOKENS` enum, consumed by both TS surfaces (TUI `GatewaySkin` and desktop dedup onto it). - Desktop: `skinToDesktopTheme` resolver (skin → CSS-var palette, VS Code-style derive-from-seed) + `backend-sync` that registers backend skins into the theme registry (Appearance/Cmd-K/`/skin`) and applies on a real change. Seeds on gateway.ready (never stomps a persisted pick), applies on skin.changed and the post-turn `config.get skin` poll (catch-all for agent-edited config.yaml). - TUI: `fromSkin` now maps the status bar + `background` keys it was dropping. - Gateway: `config.get skin` also returns the full resolved palette (additive). - Skill: `hermes-themes` teaches the agent to author + activate a skin. Each surface keeps its own normalizing resolver (ansi for the TUI, CSS vars for the desktop, prompt_toolkit/Rich for the CLI).
…aml hand-edit
The skill told the agent to `patch` display.skin into config.yaml; a stray indent
corrupts the file and breaks the live gateway (the reported "/ menu broke"), and
a raw file edit never live-applies in a running CLI/TUI ("nothing happened").
Route activation through the safe writer (`hermes config set display.skin`), and
state plainly that a tool call can't hot-switch a running CLI/TUI — the user runs
`/skin <name>` (desktop still auto-repaints on the next turn).
…cher A skin Hermes activates (`hermes config set display.skin X`) or recolors in place now goes live on every surface (CLI, TUI, desktop) within ~half a second, on its own — no `/skin`, no tool-hook timing, no user action. A gateway daemon polls the resolved skin signature `(name, active-file mtime)` every 0.5s and broadcasts `skin.changed` on any real move — a name switch OR a live color edit to the active skin. It routes through the SAME path `/skin` uses, so all surfaces repaint identically. The watcher seeds its baseline at gateway.ready (stdio + ws) so it only fires on a real change; the `/skin` RPC seeds the baseline too so it never double-broadcasts. Subsumes the desktop's post-turn `config.get skin` poll (its skin.changed handler already applies).
The TUI inherited the terminal's background; now a skin's `background` paints the whole surface via OSC 11 when a skin is applied, and clears back to the terminal default (OSC 111) on revert and on exit (ridden in through resetTerminalModes). Opt-in: a skin with no `background` leaves the terminal untouched, and the restore only fires if we actually painted. Desktop already themed its own bg; this closes the loop so Hermes owns its background on every surface.
Theming was semantic-only: the gold tool `●` was `accent`, shared with headings/links/chevrons, so "recolor tool calls" was impossible and the agent had no key to point at. Add `ui_tool` (● + tool spinner) and `ui_thinking` (reasoning body) tokens that fall back to accent/muted — defaults unchanged, but now independently settable. Make diffs skinnable too (`diff_*`), which fromSkin previously hardcoded. Document the full element→key map in the skill so Hermes knows which knob turns what.
Changing one color ("make the tool ● cyan") forked `default` — which has no
`background` — so applying it reset the terminal to its own (black) default and
dropped the active skin's palette. Teach the skill to edit the active skin's file
in place for a tweak (watcher repaints on the mtime bump), and to fork a built-in
only by carrying its full palette. Hard pitfall: never fork `default` for a tweak.
…ntouched Changing a single color kept wrecking the rest because the agent hand-authored a new skin (often from `default`, which has no `background`, resetting the terminal to black). Add `hermes skin set <key> <hex>`: edits the ACTIVE skin's one key in place (a built-in is forked into an editable copy carrying its full palette), so everything else — background included — is preserved. Plus `skin use` / `skin list`. The skill now points tweaks at this command instead of hand-authoring.
Code highlighting reused brand tokens (accent/text/border/muted), so it couldn't be themed independently. Add syntax_string/number/keyword/comment skin keys → syntax* theme tokens (defaulting to those brand tokens, so defaults are unchanged) and point the highlighter at them. Documented in the element→key map.
… pipeline Element tokens (ui_tool/ui_thinking), skinnable diffs, and code-syntax keys flow through buildPalette → adaptColorsToBackground instead of a hand-mapped color block, so they inherit NousResearch#20379's contrast/polarity machinery. thinking and syntaxComment track the EFFECTIVE muted (banner_dim override included); the skin's `background` feeds the surface (it also paints the terminal via OSC 11); statusFg falls back through ui_text/banner_text. Tests assert the routing/independence contracts rather than pre-adaptation hexes.
ingestBackendSkin returned early for name === 'default' even when apply=true, so a real runtime switch to the default skin (/skin default on CLI/TUI, or config.set display.skin=default) emitted skin.changed but never repainted the desktop. 'default' is no-opinion on the PALETTE (the desktop keeps its own nous default, so we still never register a converted theme under it), but it IS a valid apply TARGET: setTheme normalizes 'default' -> nous, so switching back repaints to the desktop default. Skip only the registry step for 'default' and let it flow through the apply guard. Addresses Copilot review.
NousResearch#65919 persists verification candidates (finish_reason=verification_required / verify_hook_continue) to state.db but collapses them out of the in-memory model history via repair_message_sequence. The eager session.resume + REST paths read the verbatim display lineage (candidate present), but the warm/live-reuse payload (_live_session_payload) built its user-visible messages from the collapsed in-memory model history — so switching to a still-live session dropped the substantive verification answer that a cold resume of the SAME session showed. That divergence is the cross-session "substantive text vanishes on switch" class, and the direct sibling of the resume-duplication regression fixed in NousResearch#68149. Reconcile the persisted display lineage (candidate-inclusive, the same get_messages_as_conversation(..., include_ancestors=True) read the eager resume + REST paths use) with the fresh in-memory tail in _live_visible_history, so all three surfaces agree by construction while a not-yet-flushed live turn is still shown. Extracted _reconcile_display_with_live as a pure, DI-testable function (anchors on the last persisted row's (role, text); appends only the uncovered in-memory tail; trusts the DB display when the tail can't be anchored). Tests: unit coverage for candidate-inclusion, freshness, empty/raising-DB fallback, and the combined candidate+fresh-tail case. The existing freshness guard (test_session_resume_live_payload_uses_current_history_with_ancestors) stays green.
… E2E Complete the NousResearch#65919 warm/live-payload fix across its sibling path and add real-SessionDB cross-builder coverage. - Child-watch (lazy) resume: the delegated-subagent watch window served _history_to_messages(repaired_history) for its user-visible messages, which collapses out persisted verification candidates just like the warm-payload path did. Build the visible messages from the verbatim child-only display projection (repair_alternation=False) while the repaired history still feeds live replay; fall back to the repaired history if the display read fails. - E2E cross-builder consistency (real SessionDB, not mocks): a persisted verification candidate is collapsed out of the model projection but kept in the display projection, and _live_visible_history now equals the eager session.resume display projection (candidate present). Adds the combined candidate + fully-flushed-second-turn case and a lazy child-watch handler test that asserts the candidate survives in resp["result"]["messages"].
The new hermes skin subcommand must be declared so startup plugin discovery can skip when the user targets it.
…-candidate-warm-payload fix(tui_gateway): candidate-inclusive display on warm/live + child-watch resume (NousResearch#65919 fallout)
…-sdk feat(ui-tui): widget-app SDK — apps as state+reducer+render, with three reference apps
feat(themes): cross-surface theme SDK — one skin themes CLI, TUI, and desktop, live
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
…d age util - Use the fork glyph for branch and a sine wave for read aloud (all one lib now) - Extract compact "2h ago" into formatAgo() in lib/time.ts (+ ageDays locale string) - Cover formatAgo with a unit test
A stray tsc run can emit foo.js next to foo.ts under apps/shared/src or apps/desktop/src. .gitignore hides the artifact from git status, but Vite resolves extensionless imports .js-before-.ts, so the renderer silently runs the stale compiled copy. tsc -b . --clean already knows the emit graph and deletes matching outputs. Run it before vite in all dev scripts. This bit for real: a Jul 16 artifact of websocket-url.js predated the NousResearch#68250 getGatewayWsUrl contract change ({ ok, wsUrl } IPC result), so its old 'if (fresh) return fresh' handed the whole result object to new WebSocket(), dialing ws://127.0.0.1:5174/[object%20Object] on every boot. The desktop app could never connect, and the failure survived reboots and cache wipes because the poison lived in src/. JsonRpcGatewayClient.connect() now rejects non-ws:// URLs with a readable error instead of letting new WebSocket() coerce an object into [object%20Object], so any future contract skew fails diagnosably.
…-actions Flatten assistant message actions into an inline icon row
…ousResearch#54242) A pure-Latin query (no CJK characters) routes to the unicode61 `messages_fts` table, whose tokenizer does not insert a boundary between Latin letters and adjacent CJK characters. Content like "修改youer服务端" is indexed as a single token, so `search_messages("youer")` returned zero results even though the substring is present, and the Latin path had no fallback. Add a zero-result trigram fallback to the pure-Latin path: when the unicode61 search misses, retry against the existing `messages_fts_trigram` table, which matches substrings regardless of word boundaries. The fallback is gated on `_trigram_available` and on every token being >=3 chars (the trigram minimum), and only fires on a zero-result miss, so successful Latin searches keep their unicode61 ranking unchanged. The trigram query construction shared with the CJK path is extracted into a `_run_trigram_search()` helper; the CJK branch is refactored to use it with no behavior change. Adds regression tests in tests/test_hermes_state.py::TestCJKSearchFallback.
The zero-result fallback prefers messages_fts_cjk when built: exact ranked token match for Latin runs unicode61 fused onto CJK, including <3-char tokens the trigram leg can't recover.
…aces Two narrow timing windows (reported by null-runner) silently downgraded a mid-turn correction to a plain next-turn message on the desktop client: - Turn-build window: a fresh turn flips running=True and builds the agent asynchronously, so session["agent"] is briefly None. session.redirect answered 4010 "unsupported", which the renderer's catch swallowed into a lost follow-up. Queue the correction server-side instead and return status="queued" — lossless, and honest about what happened. - Stale runtime id after reconnect: session.redirect 404s on a sid the gateway no longer maps. redirectPrompt now resumes the stored session and retries once, mirroring stopPrompt, so a correction fired right after a reconnect isn't dropped. The desktop treats "queued" like "redirected": the correction reaches the model either way, so it's recorded once as a real user message.
…board), not just stdio The cross-surface theme SDK's live-repaint relies on a gateway skin watcher that polls config and emits skin.changed on any move. But that emit is session-less and fires from a background thread, so write_json fell through its (session-transport -> contextvar -> stdio) ladder to the module stdio transport — which only reaches the stdio TUI (tee'd to the dashboard WS publisher). WS clients (the desktop app, dashboard chat) never got it, so 'Hermes themes itself' repainted the CLI/TUI but not the GUI. Add a live-transport registry (one entry per connected WS peer, maintained by handle_ws) and a _broadcast_global_event primitive that fans session-less announcements out to every connected client, falling back to write_json when none are registered (stdio path unchanged). Route both skin.changed emits (watcher + the /skin RPC) through it, so a skin switch from any surface repaints all of them. Backend-only; desktop already handles skin.changed and does not drop session-less events.
…ldowns (NousResearch#69494) When Codex returns 429 usage_limit_reached, Hermes persists the provider's reset_at on the pool entry and freezes the credential until it elapses -- which can be days out for weekly windows. But the upstream window can reopen EARLY: the user redeems a banked rate-limit reset (Codex CLI / ChatGPT UI), upgrades their plan, or OpenAI resets the window. Hermes never re-checked, so it kept erroring with 'Codex provider quota exhausted (429); retry after Ns' until a manual re-auth rewrote the tokens (issue NousResearch#43747, externally-reset variant). - hermes_cli/auth.py: add _probe_codex_quota_restored() -- a throttled (5 min/token) GET of the Codex /usage endpoint; quota counts as restored when every reported window is <100% used. Add clear_codex_pool_quota_cooldowns() to lift 429/quota-shaped cooldowns from persisted pool entries (DEAD and auth-shaped entries untouched). - resolve_codex_runtime_credentials(): before surfacing a pool-only cooldown as 'quota exhausted', probe upstream; on a positive probe clear the cooldown and return the pool credential. - agent/credential_pool.py: _available_entries() probes frozen openai-codex entries (clear_expired path only) and unfreezes them when upstream confirms the reset. - agent/account_usage.py: a successful /usage reset redemption now clears persisted pool cooldowns immediately. Negative paths preserved: probe 429/exhausted/indeterminate keeps the cooldown; read-only enumeration never probes; non-JWT tokens never probe (no network in hermetic tests).
…ed the activation Real-world failure from dogfooding the live-theme flow: display.skin was already 'synthwave' in config, but the desktop never visibly applied it (the activation event predated the WS transport fix / the connect). The desktop's gateway.ready seed records the baseline WITHOUT painting (by design — never stomp the persisted desktop theme on connect), so it believed it was synced. Re-running 'hermes config set display.skin synthwave' then did nothing twice over: the watcher signature (name, skin-file mtime) hadn't moved, so no skin.changed fired; and even on an event, the desktop's name-equality guard blocked the apply against the seeded baseline. Two halves: - hermes_cli: setting display.skin touches the named skin file so the watcher signature always moves on an explicit set — a same-name re-affirm now broadcasts skin.changed like any real move. Built-ins (no file) are unaffected; a name switch already moves their signature. - desktop: track whether the synced baseline was actually APPLIED vs merely seeded at connect. A skin.changed matching a seed-only baseline is an intentional apply and repaints; once applied, repeat same-name events stay no-ops (protects a manual desktop-side theme switch from snap-back, incl. across a reconnect re-seed).
…hadow-guard fix(desktop): clean stale tsc emit + guard gateway WS URLs
…ousResearch#54855) (NousResearch#67364) * chore(gitignore): ignore installer .install_method stamp Salvage of NousResearch#54855 by @drissman — rebased onto current main with root-scoped rule and sister-marker comments alongside .update-incomplete. Closes NousResearch#66189 Root cause: scripts/install.sh writes <install>/.install_method but git did not ignore it, so managed checkouts show ?? .install_method and hermes update may autostash the untracked marker. Fix: add /.install_method to .gitignore (repo-root only). Verification: git check-ignore -v .install_method * test(update): assert .install_method survives update autostash (NousResearch#66189) Add hermetic regression mirroring the .hermes-bootstrap-complete test: adopt the real .gitignore, drop the installer .install_method stamp, run the exact 'git stash push --include-untracked' the updater uses, and assert the marker is neither swept nor reported dirty. Requested by hermes-sweeper review on NousResearch#67364.
…n-steering feat: redirect active turns when users correct the agent
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
…ighten comments _emit and _broadcast_global_event were each building the JSON-RPC event envelope — extract _event_frame and use it from both. Type the registry as set[Transport] (protocol already imported), and cut comment bloat at the call sites. No behavior change; suites stay green.
…broadcast fix(themes): live skin sync reaches every surface — WS fan-out + missed-activation recovery
…lternate (#4) HERMES.md (hand-authored machine operating manual) and AGENTS.md (auto- generated Layer-0 nav index) serve distinct roles, but build_context_files_prompt loaded them via a first-match-wins or-chain. From a CWD where both exist (e.g. ~ on this machine), HERMES.md won and AGENTS.md was silently dropped — while every doc (HERMES.md §8, SOUL.md, and AGENTS.md's own header) claims AGENTS.md is auto-loaded. Now HERMES.md loads as primary project context, and AGENTS.md loads alongside it when both are present. The CLAUDE.md/.cursorrules fallback chain retains first-match-wins semantics (those ARE ecosystem equivalents). Security: AGENTS.md still passes through _scan_context_content (via _load_agents_md) before injection — no bypass of the prompt-injection guard. Co-authored-by: Hermes Agent <kathanc99@icloud.com>
Agent jobs (script:null, run inside run_conversation) had no retry — a
transient LLM timeout/rate-limit/network blip failed the job and
mark_job_run re-armed next_run_at to the next cron tick (hours away).
This adds an optional per-job retry config:
retry: {max_attempts: N, delay_seconds: S}
On failure, the scheduler re-arms next_run_at to now+delay (up to N
times) before reverting to the cron schedule. The normal ticker picks up
the retried job via its existing next_run_at<=now due-check — no new
queue, no new thread, no due-check change.
Design (minimal, rides existing mechanisms):
- jobs.py: add retry config field + retry_count to create_job; reset
retry_count on success in mark_job_run. Validation rejects max_attempts<1
or delay_seconds<1.
- scheduler.py: _record_job_outcome helper wraps both mark_job_run call
sites in run_one_job. Always records the honest outcome first, then
post-overrides next_run_at when a retry is due (mark_job_run's own
next_run_at=comput_next_run would otherwise clobber it).
- The interrupted-flag consume stays OUTSIDE the helper so an
interrupted run is never retried.
Opt-in: jobs without a retry field hit the exact same path as today.
The non-regression test (test_no_retry_config_failure_preserves_behavior)
pins this. Full tests/cron/ suite: 668 passed, 9 pre-existing env failures
(croniter-missing + gateway-env tests) unchanged from clean main.
7 new tests in TestJobRetry cover: config persistence, validation rejection,
re-arm on failure, reset on success, exhaustion, and the non-regression guard.
Co-authored-by: Hermes Agent <kathanc99@icloud.com>
…l + signatures Upgrade-catchup fixups for tests that broke against upstream a41d280 for reasons unrelated to the customizations' runtime behavior: - test_cron_script.py (3 Windows/decoding tests): upstream added these tests asserting subprocess.run argv/kwargs, but the stall-detection commit (5c57bda) rewrote _run_job_script to stream via subprocess.Popen. Port the mocks to a Popen stand-in (_make_fake_popen) exposing readable stdout/stderr streams + poll()/wait(). Assertions on interpreter/env/creationflags/encoding are unchanged, so they now positively verify the merge preserved upstream's _windows_cron_python_invocation + env_overlay + win32 encoding. - test_script_claim_heartbeat.py (3 tests): upstream-only tests for its _run_job_script_with_claim_heartbeat wrapper. The rebase threads progress_key through that wrapper (so one-shot claimed scripts still publish stall/progress telemetry), so its _run_job_script mocks must accept progress_key. Runtime behavior and the claim-refresh assertions are unchanged. - test_native_delivery_receipt_integration.py (2 tests): the 1-arg mock `lambda _path:` predates progress_key (added by 5c57bda); it was already red on fork main. Accept progress_key so run_job's call reaches the mock. - test_adapter_runtime_inventory.py (1 test): the _make_adapter_auth_check mock predates upstream commit f57157a adding a profile_name kwarg. Accept profile_name so the multiplex path reaches its redaction assertion. No product code changed. Kept separate from the 6 customization commits so it can be squashed or dropped independently. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(cron): make _stop_script_process Windows-footgun-clean The stall-detection commit used bare os.killpg + signal.SIGKILL, which fails upstream's blocking windows-footguns lint on every fork PR. Resolve both via getattr (killpg=None fallback to terminate/kill; SIGKILL->SIGTERM fallback) — POSIX behavior byte-identical, checker now passes: 789 files scanned, 0 findings. Scheduler tests: 266 passed. * chore(contributors): map nima@girnarholdings.com -> girnarholdings The machine's verified git identity (nima@girnarholdings.com) was unmapped, failing the contributor-attribution check on every fork PR. Owner Kathan Thakkar confirmed this identity is the desk's trusted machine identity. --------- Co-authored-by: nima <nima@girnarholdings.com>
* feat(prompts): subagent fleet doctrine — evidence-bound summaries + shared-checkout git discipline Two injection points, prompted by 2026-07-21 fleet collisions (staged-index sweep, branch-from-moved-HEAD, web-UI conflict resolution silently dropping a merged PR's entire diff): 1. delegate_tool._build_child_system_prompt: new 'Honesty and evidence' block (self-reports must cite observable evidence; side-effecting ops need verifiable handles; 'cannot verify' beats fabricated certainty) and 'Git discipline in shared checkouts' block (verify HEAD first, isolate via worktree/branch, explicit-path add+commit immediately, never leave staged state, restore checkout on exit). 2. prompt_builder.KANBAN_GUIDANCE: new 'Shared-checkout git discipline' section for kanban workers (same rules + never web-UI conflict resolution — a resolution is a code change; leave checkout on main so nightly automation isn't broken). Tests: tests/agent/test_prompt_builder.py (166) + 6 delegate suites (212) all pass. * test(kanban): raise KANBAN_GUIDANCE size ceiling for git-discipline section The subagent fleet doctrine PR adds a 'Shared-checkout git discipline' section to KANBAN_GUIDANCE, growing it from under 5_500 to 6_783 chars. The bound test's docstring already states the ceiling guards against unbounded growth, not any growth — raise it to 8_000 to fit the new intended content with ~18% headroom. --------- Co-authored-by: nima <nima@girnarholdings.com>
…ad-target alerting (#8) Budget-aware flood-control retries (both send paths), send raw text as plain instead of unescaped MarkdownV2 to kill parse-fallback double-sends, and a loud grep-able DEAD-TARGET WARNING (with INFO self-heal). Evidence: 2026-07-19 04:04-04:05 errors.log (retry_after 249-255s dropped on attempt 3; ~40 'character (' is reserved' fallbacks).
…ver block + fork observer) GitHub could not auto-merge the rebased branch into pre-rebase main: both carry the same 10 customizations as different commits, and base.py was the one textual divergence (main lacks upstream's platform-lock takeover block). Resolution keeps the branch side, which contains BOTH features; verified: observer + takeover attributes coexist, ruff clean, footguns clean, adapter-inventory + dead-targets suites green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
girnarholdings
pushed a commit
that referenced
this pull request
Jul 24, 2026
…e_check_xsrf pitfalls Add two pitfalls discovered when running the skill against a fresh Jupyter server: - Pitfall #9: When the websocket reply channel hangs on every execute even though the kernel actually ran (REST shows execution_state=idle and execution_count increments), force zmq transport with --transport zmq. The zmq transport uses jupyter_client directly and sidesteps the broken websocket layer. - Pitfall #10: A fresh ServerApp rejects POST /api/sessions with "_xsrf argument missing from POST" unless you start it with --ServerApp.disable_check_xsrf=True. Needed for REST-only flows where no browser/cookie is establishing the XSRF token.
girnarholdings
pushed a commit
that referenced
this pull request
Aug 3, 2026
- tests/agent/test_session_activity.py asserts against ACTIVITY_DESCRIPTION_MAX instead of the literal 120. - The session-stall WARNING log line names its config knob (agent.session_stall_timeout) so operators can find the setting. - hermes_state.py: collapse the triple blank line near line 191. - hermes_cli/status.py no longer imports the private hermes_cli.main._relative_time: the helper moved to a public home (hermes_cli.timefmt.relative_time); main._relative_time stays as a thin back-compat wrapper (sessions_cmd and external patchers keep working).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this does
Rebases the fork's 10 local customization commits from the old upstream pin
a41d280f9(2026-07-20) onto the current NousResearch upstream tipe0b9ab5ac(2026-07-22, PR NousResearch#69533) — 402 upstream commits of catch-up.Because the previous fork upgrade had already pinned to
a41d280f9, themerge-base of
mainandupstream/mainis exactlya41d280f9, so this is aclean linear catch-up: the 10 commits replay directly onto the new tip. No
rebase fallback (merge / cherry-pick) was needed.
Shape:
HEADis now[ahead 10, behind 0]ofupstream/main.Commit-by-commit disposition
8d605ad469ae380db9=)dfcf7b5289d35cd3437d7ad174271f598ae8gateway/platforms/base.py), see below37fe1a5cd0a8daba0825e1d3aa8f9de29e5a0c86dc7a251d7bbba01604014865f96e56df34df1b3555da4ff0d02e76494476e1d05aefcaa2333a6937e2a389git range-diff upstream/main main HEADreports 9/10 commits identical (=) andonly commit #3 changed (
!). No commit was dropped or became redundant — nonewent empty against upstream (verified), i.e. upstream has not independently
implemented any of these features. The hot file
cron/scheduler.py(which carriesretry / delivery-receipts / stall-detection) had 0 upstream commits since the
merge-base, so all three scheduler-heavy customizations replayed without conflict.
The only conflict —
gateway/platforms/base.py(commit #3)Both sides insert an independent, additive block into
BasePlatformAdapter.__init__, immediately afterself._fatal_error_handler = None:155fdd59f— "take over live platform-lock token holders"):7d7ad1742— adapter runtime inventory):Resolution: keep both blocks (upstream's takeover fields first, then our
observer field). They are orthogonal — different features, no shared state. The
commit's other three hunks to this file (the
AdapterFatalErrorimport, theset_adapter_runtime_observer/_notify_adapter_runtimemethods, and theconnected/disconnected/errornotify calls) landed in regions upstream didnot touch and applied cleanly. Upstream's own additions to this file (the
weakrefimport,bws_cache.enc.jsonredaction path, the_acquire_platform_locktakeover logic, and the
SessionSourcetransport-provenance weakref) are allpreserved.
python -m astparses the resolved file; the observer contract isexercised green by
test_adapter_runtime_inventory.pyandtest_telegram_network_reconnect.py.Invariants confirmed intact post-resolution: all
_notify_adapter_runtimehookspresent (10 refs in the telegram adapter, 4 in
base.py); redaction seamunchanged (callback still receives lifecycle names + exception classes only).
Test results (run the way CI runs them)
Environment:
uv sync --locked --python 3.11 --extra all --extra dev(exit 0 —this also proves
uv.lockis in sync withpyproject.toml; neither file istouched by our commits). Tests run via the canonical
scripts/run_tests.sh(per-file isolation,
OPENROUTER_API_KEY/OPENAI_API_KEY/NOUS_API_KEYblanked),matching
.github/workflows/tests.yml.Blocking lint gates (
.github/workflows/lint.yml):ruff check .→ All checks passed (exit 0)python scripts/check-windows-footguns.py --all→ No footguns, 800 files (exit 0)Targeted suites — every touched area:
Import smoke of the resolved runtime (
cron.scheduler,cron.delivery_receipts,gateway.run,gateway.adapter_runtime,gateway.platforms.base,gateway.dead_targets,agent.prompt_builder,tools.send_message_tool,tools.delegate_tool) all import clean.Not run locally (out of scope / infra-bound, run by CI on this PR): the full
tests/gateway,tests/agent,tests/toolsdirs (500+ files each), thee2e/desktop suites, docker/osv/supply-chain jobs.Mergeability verdict
MERGEABLE — recommend merge once CI is green. This is a clean linear rebase:
9/10 commits are byte-identical to their already-reviewed originals, and the sole
conflict is a trivial two-block additive coexistence in one file, verified by
tests that exercise both features. All locally-runnable CI gates pass.
Residual risk (low):
gateway/platforms/base.py: our runtime observer and upstream's platform-locktakeover now share
__init__. They touch disjoint state;test_adapter_runtime_inventory.py(6✓) and
test_telegram_network_reconnect.py(47✓) pass. No shared code path.on this PR is the backstop. The touched files within them are all green.
~/.hermes/config.yaml) references the stall-detection keys fromcommit feat(prompts): subagent fleet doctrine — evidence + git discipline #6; those survive the rebase unchanged (0 upstream churn on
scheduler.py/
config.pystall keys).DO NOT MERGE until CI on this PR is green. Merge is operator-gated per the
fork-sync runbook; the live fast-forward is a separate, human-gated maintenance step.
Rebased branch base =
e0b9ab5ac(upstream/main). Worktree-only; live gatewaytree at
~/.hermes/hermes-agentwas never switched or edited.🤖 Generated with Claude Code