Skip to content

upgrade: rebase fork customizations onto upstream e0b9ab5ac (2026-07-22) - #9

Merged
girnarholdings merged 413 commits into
mainfrom
upgrade/upstream-20260722
Jul 22, 2026
Merged

upgrade: rebase fork customizations onto upstream e0b9ab5ac (2026-07-22)#9
girnarholdings merged 413 commits into
mainfrom
upgrade/upstream-20260722

Conversation

@girnarholdings

Copy link
Copy Markdown
Owner

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 tip
e0b9ab5ac (2026-07-22, PR NousResearch#69533) — 402 upstream commits of catch-up.

Because the previous fork upgrade had already pinned to a41d280f9, the
merge-base of main and upstream/main is exactly a41d280f9, so this is a
clean linear catch-up: the 10 commits replay directly onto the new tip. No
rebase fallback (merge / cherry-pick) was needed.

Shape: HEAD is now [ahead 10, behind 0] of upstream/main.

Commit-by-commit disposition

# Original SHA Rebased SHA Subject Disposition
1 8d605ad46 9ae380db9 fix(prompt): load AGENTS.md alongside HERMES.md (#4) Replayed clean — byte-identical (range-diff =)
2 dfcf7b528 9d35cd343 feat(cron): retry-on-failure for agent jobs, opt-in (#5) Replayed clean — byte-identical
3 7d7ad1742 71f598ae8 feat(gateway): persist redacted adapter runtime inventory Conflict-resolved — 1 file (gateway/platforms/base.py), see below
4 37fe1a5cd 0a8daba08 feat(cron): add native target-bound delivery receipts Replayed clean — byte-identical
5 25e1d3aa8 f9de29e5a fix(cron): keep failure alerts out of artifact receipts Replayed clean — byte-identical
6 0c86dc7a2 51d7bbba0 fix(cron): stream script progress and detect stalls Replayed clean — byte-identical
7 160401486 5f96e56df test(cron,gateway): reconcile fork tests w/ upstream model Replayed clean — byte-identical
8 34df1b355 5da4ff0d0 fix(cron): make _stop_script_process Windows-footgun-clean (#7) Replayed clean — byte-identical
9 2e7649447 6e1d05aef feat(prompts): subagent fleet doctrine (#6) Replayed clean — byte-identical
10 caa2333a6 937e2a389 fix(telegram): retry-budget/MarkdownV2/dead-target (#8) Replayed clean — byte-identical

git range-diff upstream/main main HEAD reports 9/10 commits identical (=) and
only commit #3 changed (!). No commit was dropped or became redundant — none
went empty against upstream (verified), i.e. upstream has not independently
implemented any of these features. The hot file cron/scheduler.py (which carries
retry / 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 after self._fatal_error_handler = None:

  • Upstream (155fdd59f — "take over live platform-lock token holders"):
    self._platform_lock_takeover_allowed = False
    self._platform_lock_takeover_attempted = False
  • Ours (7d7ad1742 — adapter runtime inventory):
    self._adapter_runtime_observer: Optional[
        Callable[[str, Optional[type[BaseException]]], None]
    ] = None

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 AdapterFatalError import, the
set_adapter_runtime_observer / _notify_adapter_runtime methods, and the
connected/disconnected/error notify calls) landed in regions upstream did
not touch and applied cleanly. Upstream's own additions to this file (the
weakref import, bws_cache.enc.json redaction path, the _acquire_platform_lock
takeover logic, and the SessionSource transport-provenance weakref) are all
preserved. python -m ast parses the resolved file; the observer contract is
exercised green by test_adapter_runtime_inventory.py and
test_telegram_network_reconnect.py.

Invariants confirmed intact post-resolution: all _notify_adapter_runtime hooks
present (10 refs in the telegram adapter, 4 in base.py); redaction seam
unchanged (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.lock is in sync with pyproject.toml; neither file is
touched by our commits). Tests run via the canonical scripts/run_tests.sh
(per-file isolation, OPENROUTER_API_KEY/OPENAI_API_KEY/NOUS_API_KEY blanked),
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 --allNo footguns, 800 files (exit 0)

Targeted suites — every touched area:

tests/cron (full, 35 files) + adapter_runtime_inventory + dead_targets
+ telegram_network_reconnect + telegram_thread_fallback
+ send_message_flood_budget + send_message_parse_fallback
+ kanban_tools + prompt_builder
→ 43 files, 1191 tests PASSED, 0 failed (100%)

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/tools dirs (500+ files each), the
e2e/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):

  1. Semantic, not textual — the only feature interaction worth an eyeball is in
    gateway/platforms/base.py: our runtime observer and upstream's platform-lock
    takeover 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.
  2. Full gateway/agent/tools suites were not run locally (500+ files each) — CI
    on this PR is the backstop. The touched files within them are all green.
  3. Live config (~/.hermes/config.yaml) references the stall-detection keys from
    commit feat(prompts): subagent fleet doctrine — evidence + git discipline #6; those survive the rebase unchanged (0 upstream churn on scheduler.py
    / config.py stall 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 gateway
tree at ~/.hermes/hermes-agent was never switched or edited.

🤖 Generated with Claude Code

OutThisLife and others added 30 commits July 21, 2026 20:29
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>
OutThisLife and others added 27 commits July 22, 2026 12:18
…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
girnarholdings merged commit 720365b into main Jul 22, 2026
79 of 84 checks passed
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).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.