Skip to content

Sync current Nous Hermes main into Ace patches - #39

Closed
vashkartik wants to merge 5854 commits into
ace/patchesfrom
automation/hermes-nightly-sync
Closed

Sync current Nous Hermes main into Ace patches#39
vashkartik wants to merge 5854 commits into
ace/patchesfrom
automation/hermes-nightly-sync

Conversation

@vashkartik

Copy link
Copy Markdown
Owner

Automated fail-closed upstream sync. Locally verified head: 2be2098

zzy-git and others added 30 commits August 4, 2026 14:34
…d V4A patch path

write_file currently spawns up to 6 subprocesses per call:
  1. mkdir -p (separate call before atomic write)
  2. cat (to read pre-content for lint/BOM/line-ending detection)
  3. _atomic_write (mktemp + write + mv — the essential one)
  4. wc -c (to measure bytes written)
  5. _check_lint_delta (post-write lint — also essential)
  6. LSP snapshot (also essential)

This PR removes three of them without changing any observable behavior:

1. Fold mkdir -p into _atomic_write shell script (−1 subprocess/write)
   The atomic write script already runs a single shell; adding mkdir -p
   to it costs zero extra processes.

2. Add optional pre_content parameter to write_file (−1 subprocess/patch)
   patch_replace and V4A _apply_update already read the file for fuzzy
   matching. Passing that content as pre_content skips the redundant cat
   inside write_file. Fully backward-compatible: callers that don't pass
   pre_content still read from disk as before.

3. Replace wc -c with len(content.encode('utf-8')) (−1 subprocess/write)
   We already have the content in memory; encoding it to get the byte count
   is equivalent to wc -c for UTF-8 text.

4. Remove redundant _check_lint loop in apply_v4a_operations (−N subprocesses/V4A)
   write_file already runs _check_lint_delta internally. The old code ran a
   bare _check_lint(f) loop over all modified files — a re-read + re-lint
   without post_content context. Now lint results propagate from write_file
   via a four-tuple return, zeroing out the extra subprocesses.

Net effect:
  - write_file: 6 → 3 subprocesses per call (new files)
  - patch_replace: 6 → 5 subprocesses per call (pre_content skips cat)
  - V4A multi-file patches: saves 1 subprocess per modified file
  - A typical 4-file V4A patch drops from ~28 to ~16 subprocess calls
…ard compat

Bug 1 (UTF-8 BOM loss on V4A UPDATE):
_file_has_bom() trusted pre_content for BOM detection, but the most
common pre_content provider — read_file_raw() — deliberately strips
BOMs so the agent never sees U+FEFF glyphs.  Passing BOM-stripped
content through pre_content caused a false-negative: the method
returned False and write_file() silently removed the marker on rewrite.

Fix: _file_has_bom() now always probes the first 3 bytes on disk
(head -c 3), ignoring pre_content for BOM purposes.  pre_content is
still used by two other consumers — line-ending detection and lint/LSP
delta computation — neither of which is affected by BOM stripping.

Bug 2 (backward compatibility):
_apply_update() called write_file(path, content, pre_content=...) as a
keyword argument.  Duck-typed file_ops implementations that only
implement the two-argument write_file(path, content) contract would
raise TypeError.

Fix: wrap the call in try/except TypeError, falling back to the
two-argument form when the keyword is not accepted.

Also declare tomli in pyproject.toml (pre-existing conditional import
for pre-3.11 Python, caught by the pre-commit dep scan after staging
file_operations.py).

Tests:
Add TestV4ABomRoundTrip with two cases:
  - UPDATE on BOM-bearing file preserves the marker
  - UPDATE on plain file does not inject a BOM

Addresses teknium1 review on PR NousResearch#55661.
Content that flowed through a surrogateescape decode (backend output via
patch_replace) can carry lone surrogates; a strict encode raises
UnicodeEncodeError where the old wc -c path could not. Mirrors the
existing sha256 verification encode.
- write_file: encode content once, share bytes between bytes_written and
  the sha256 verification (drops a second full-content encode per write)
- patch_parser: replace the except-TypeError retry around
  write_file(pre_content=...) with signature-based feature detection so a
  TypeError raised inside a capable implementation propagates instead of
  triggering a duplicate write; tests for both duck-typing contracts
- tests: real-ops V4A BOM round-trip + _file_has_bom disk-probe guard
  (the teknium1-review regression previously only covered by a fake)
- comment: document dirs_created's long-standing "parent ensured" meaning
requires-python is >=3.11 so tomllib is always in stdlib; the
tomli fallback branch in _lint_toml_inproc was unreachable. Removes
the dependency from pyproject.toml + uv.lock and deletes the dead
try/except ImportError fallback in the code.
…warning

NousResearch#75017: Telegram polling conflict retry used drop_pending_updates=False,
starting a new getUpdates session that immediately got 409'd by the
previous still-expiring session — creating the very conflict it was
trying to recover from. Switch to drop_pending_updates=True so Telegram
terminates stale sessions. Also add a recovery-generation guard so the
first transient getUpdates success after a retry doesn't reset the
conflict counter back to 0 (defense-in-depth from PR NousResearch#75096).

NousResearch#75153: The WAL-reset warning always said 'hermes update can repair'
even for git/pip/system Python installs where it can't. Now uses
detect_install_method() + recommended_update_command_for_method() to
give a context-appropriate hint (hermes update for git, docker pull for
docker, nix message for nix, generic install hint as fallback).
…-junhohong

chore: contributor email mapping for junhohong
Loopback dashboard tabs now share one one-shot stale-token recovery path across REST 401s, the PTY socket, the structured event socket, and the shared JSON-RPC gateway wrapper. The shared client exposes only an optional close-event interception hook; the dashboard remains responsible for deciding that loopback 4401 means reload.

Constraint: Current main delegates the web gateway to apps/shared JsonRpcGatewayClient, and NousResearch#54022 review requires a shared-client-compatible close-code hook plus direct ChatSidebar event-socket coverage.
Rejected: Restore the dashboard's old direct WebSocket implementation | stale against the shared JSON-RPC client and would duplicate transport behavior.
Confidence: high
Scope-risk: moderate
Directive: Keep stale-token policy dashboard-specific; the shared JSON-RPC client should expose close events without learning dashboard auth semantics.
Tested: npm --workspace web test (21 files, 106 tests); focused stale-token tests (5 files, 14 tests); npm --workspace web run typecheck; npm --workspace @hermes/shared run lint; npm --workspace @hermes/shared run typecheck; focused web eslint; git diff --check.
Not-tested: Manual browser smoke test across a real dashboard restart.
react-router v7 exports MemoryRouter from 'react-router', not
'react-router-dom'. The test was written when the repo still imported
from 'react-router-dom' (4000+ commits ago).
Sibling site missed by PR NousResearch#54022 — /api/console WebSocket in
HermesConsoleModal.tsx has the same buildWsUrl → stale-token → 4401
close path as the PTY and events WebSockets. Without this guard,
opening the console after a dashboard restart shows 'Console closed
(4401). auth: token_mismatch' with no recovery.
When Grok runs on xAI Responses, only swap to native server-side
web_search when the active/configured backend is xai. For Firecrawl
and other Hermes providers, keep client dispatch under a renamed wire
tool so Grok cannot hijack web_search and ignore user config.
Lock in backend preference, wire-name aliasing, and normalize mapping
so configured non-xai search providers stay on the Hermes client path.
Also init conflict-recovery generation on the telegram bare-adapter
helper so CI polling progress tests do not AttributeError.
Drop the manual web.search_backend / web.backend config-reading block
that duplicated _read_config_key in web_search_registry.py. The function
now delegates directly to get_active_search_provider() (which reads the
same config keys via the registry's canonical resolver) and falls back
to _get_search_backend() only when the registry has no providers loaded.

Also updates the TestXaiWebSearchBackendPreference tests to monkeypatch
the registry instead of load_config_readonly, and adds two new tests for
the legacy fallback path (no provider registered -> _get_search_backend).
Signed-off-by: Bryan Bednarski <bbednarski@nvidia.com>
hermes model saves custom_providers models: {default: {context_length}} for
local Ollama. That dict shape was treated as an explicit catalog, so no-key
endpoints skipped live /v1/models probing and Desktop/Telegram only showed
the saved default — Refresh could not help. Keep list/string shapes as
allowlists; pin dict catalogs with discover_models: false.
…urrent-turn-scopes

fix(relay): avoid concurrent turn scope corruption
…lay-model-metrics

feat(observability): report model and provider usage
Signed-off-by: Alex Fournier <afournier@nvidia.com>
Hitting ⌘1 (or cycling ⌃Tab onto the main tab) while Capabilities /
Messaging / Artifacts covered the workspace looked dead: the workspace
pane was already the zone's active tab behind the page, so fronting it
changed nothing on screen.

activateTreeTabSlot / cycleTreeTabInFocusedZone now return the activated
pane id, and the keybind handlers route back to the loaded session (or
the new-chat draft) when the landing pane is the workspace under a full
page — the same rule openSession already applies.
…fyNative

Desktop plugins can toast in-app (host.notify) but have no sanctioned way to
reach the OS notification pipeline the app's own approval/turn alerts use, so
a plugin surfacing a genuinely notable background event (e.g. a discovery
plugin finding a match) stays invisible once the user steps away from Hermes.

Add a curated per-plugin door instead of exporting the raw dispatcher:

- ctx.notifyNative({ title, body?, silent? }) on PluginContext — attributed
  to the plugin id, routed through dispatchNativeNotification so every
  existing gate applies (master + per-kind prefs, post-connect baseline,
  away-from-app gating, throttle).
- New 'plugin' native-notification kind with its own Settings ▸ Notifications
  toggle (default on), so users silence plugins without losing app alerts.
- New optional `tag` discriminator on the notify payload keys the renderer
  throttle and main-process cross-window dedupe per plugin, so two plugins
  can't collapse each other's session-less notifications.

Consumer: the Index Network desktop plugin wants background opportunity
alerts; anything in ~/.hermes/desktop-plugins gets the same door.
…gets

messageRenderWeight moves out of thread/list.tsx into lib/render-weight.ts.
The DOM page budget already spends render cost rather than message count —
the store window added next needs the same currency, and one weight function
keeps the two layers from drifting apart.

No behavior change.
…st (NousResearch#55191)

An oversized session rebuilt an unbounded runtime repository on every store
update and exhausted the renderer's V8 heap, crash-looping the window. The DOM
budget in thread/list.tsx bounds what PAINTS, but every message was still
normalized into the repository first, so a session only had to be heavy — not
visible — to kill the renderer.

selectTranscriptWindow keeps the tail that fits one render-weight page. Weight,
not message count: measured against a real 1,175-session store, a 400-message
cap disengages on 37 sessions that are heavy but short (one is 133 messages /
1.05MB) while firing on 92 long-but-light sessions that were never at risk.

The cut aligns off branch-group boundaries. useRuntimeMessageRepository records
a group's fork point the first time it sees the group, so a window starting
mid-group would re-parent the surviving branches to whatever happened to
precede them.

Co-authored-by: HexLab <8422520+HexLab98@users.noreply.github.com>
…rom the store

Show earlier spends the already-materialized DOM budget first and only asks the
session store for another page once that is exhausted, so the click stays cheap
and the store window stays as small as it can be.

Paging has no ceiling: each expand grows the window by one budget page until the
whole transcript is loaded. Branch persistence stays wired throughout —
setMessages is never dropped, so switchToBranch and applyBranchVisibility keep
working on a windowed session.

Co-authored-by: HexLab <8422520+HexLab98@users.noreply.github.com>
Fold ctx.notifyNative into a ctx.os namespace so every way a plugin
reaches outside the app window lives behind one attributed door instead
of accreting one top-level ctx method per capability:

- ctx.os.notify — the native-notification door from the previous commit,
  unchanged semantics (plugin kind pref, away-gating, per-plugin throttle).
- ctx.os.openExternal / ctx.os.revealPath / ctx.os.writeClipboard — the
  existing window.hermesDesktop bridge capabilities, now sanctioned and
  result-shaped: each resolves false (never throws) when the bridge or
  member is missing, so a plugin branches on the result instead of
  sniffing the preload surface or crashing on an older shell.

No new Electron surface: everything routes through bridge members the
app already ships; the notification path keeps every existing gate.
…earch#22622)

* fix(credential-pool): clear exhaustion state on key rotation

When a user rotates an API key (e.g. via `hermes setup` after hitting a
rate limit), _upsert_entry updates the access_token on the existing pool
entry but preserves the stale last_status=exhausted from the old key.
On the next session the pool finds the entry, sees it exhausted, and
returns no usable credentials — even though the new key is valid.

Fix: when access_token changes on an existing entry, reset last_status,
last_error_code, last_error_reason, last_error_message, and
last_error_reset_at. The exhaustion state belongs to the old key, not
the new one.

* chore: add pasevin@gmail.com to AUTHOR_MAP

* fix: clear last_status_at on key rotation, remove unused pytest import

Address review feedback from teknium1 on PR NousResearch#22622:
- Add last_status_at=None to the reset block (matches all other
  token-sync reset paths in credential_pool.py)
- Assert last_status_at is None in the regression test
- Remove unused pytest import flagged by ruff + ty
OutThisLife and others added 26 commits August 6, 2026 22:02
…summary-blocks

Reasoning steps read as separate blocks again instead of one glued paragraph
…ssion-integrity

fix: preserve session history when a turn crashes
After `hermes update`, the desktop sidebar showed "No sessions yet" until
the user's first message. NousResearch#72424 added sessions.last_activity_at, which
list_sessions_rich now selects — but column adds only land through
_reconcile_columns() in the writable _init_schema, and read-only opens
skip that by design. Every sidebar read path opens state.db read-only, so
each poll raised "no such column: s.last_activity_at" until the first
prompt's lazy session-row persist forced a writable open and reconciled.

A heal for exactly this class already existed (_open_session_db_for_profile
probes the read-only handle and does a one-time writable reopen on
staleness), but its probe was a hand-written four-column list that never
learned last_activity_at — it went stale three days after shipping. And the
batched sidebar route (/api/profiles/sessions/sidebar) bypassed the helper
entirely, swallowing per-profile failures into an errors array the desktop
never surfaces, so the incident produced an empty sidebar with clean logs.

The fix removes the maintenance burden instead of paying it once more:

- hermes_state_schema.schema_read_probe_statements() derives one
  `SELECT <every declared column> FROM <table> LIMIT 0` per table from
  SCHEMA_SQL via the existing _parse_schema_columns() — the same source of
  truth the writable reconciler diffs against, so any future ADD COLUMN is
  probed with no list to update. Column references are table-qualified:
  an unqualified double-quoted identifier that fails to resolve silently
  degrades to a string literal (SQLite's double-quoted-string misfeature)
  and would make the probe pass on exactly the store it exists to catch.

- web_server splits the heal into a path-level _open_session_db_at_path
  (semantics unchanged) so the cross-profile session routes can share it;
  both profiles.py loops and _count_status_active_sessions (the remaining
  raw read-only sibling) now open through it. The heal stays a helper
  rather than a SessionDB classmethod on purpose: escalation-to-writable
  must remain an explicit caller decision — update_cmd.py opens read-only
  mid-update and must never write.

- Exhaustion guard: if the writable heal SUCCEEDS and the re-probe still
  fails (a schema problem ADD COLUMN cannot express), the store is marked
  exhausted — warn once, skip the probe, serve reads probe-less — instead
  of re-running the full writable init on every poll against a possibly
  live DB. A FAILED writable open (transient lock) is deliberately not
  recorded, so the next poll retries the heal.

- The per-profile swallow sites in profiles.py now also log a deduplicated
  warning, so a persistent read failure is loud in errors.log even though
  the response errors array stays invisible to the sidebar.

Tests: probe/SCHEMA_SQL coverage invariants (tests/test_schema_read_probe.py),
last_activity_at added to the /api/sessions heal parametrize, a sidebar-route
heal test reproducing the shipped symptom (errors == [] and the session
returned against a store missing the column), and an exhaustion test pinning
exactly one writable open. The sidebar and last_activity_at tests fail on
main.
…tale-schema-probe

fix(desktop): derive the stale-schema read probe from SCHEMA_SQL
…ng standards

The in-repo skill-authoring skill taught the validator's ceilings (1024-char
descriptions, 'Use when ...' phrasing) instead of the repo's review standards,
so agents following it produced skills that fail review: 240+ char
descriptions, author 'Hermes Agent' with no human credit, no bundled-vs-
optional decision, dangling related_skills, no platforms audit, no tests, no
docs regen, and machine-local /home/bb/... paths baked into prose.

Rewritten to teach the hardline standards from AGENTS.md:
- description <= 60 chars, one sentence, ends with period
- author credits the human contributor first
- bundled vs optional tier decision (5+ sessions/month bar; default optional)
- no router/index/hub skills
- platforms: audited against actual scripts, POSIX-signal table
- related_skills must resolve in-repo
- Hermes-tool framing instead of raw shell prose
- tests at tests/skills/ + docs regen with scope discipline
- removed machine-local paths; validator limits marked as NOT the standard
…pora

Inspired by virgiliojr94/book-to-skill (MIT): /learn now picks the skill
shape by the source. Workflows and small sources still get one tight
SKILL.md; books, paper stacks, specs, and large doc corpora get a
knowledge-base layout — a lean always-loaded SKILL.md index plus one
distilled file per chapter/topic under references/, loaded on demand via
skill_view so query cost stays proportional to the answer.

- agent/learn_prompt.py: new _KNOWLEDGE_SKILL_STANDARDS block (index +
  per-chapter references/, structure-not-summary distillation, never
  reproduce source passages, fold-in instead of duplicating) and a
  _SOURCE_HYGIENE block pinning extracted source text as data and
  dropping invisible/bidi Unicode (Trojan Source class). Clarified that
  the ~200-line cap and hub-skill ban apply to SKILL.md itself, not a
  knowledge skill's own references/ files.
- tests: contracts for the knowledge-base layout, the three embedded
  standards blocks, and the source-hygiene coverage.
- docs: skills.md documents the knowledge-base shape.
A manual cronjob run executed the job synchronously on the calling
agent's tool thread. A cron job is a full agent run that routinely
takes minutes to hours, so the parent turn sat inside ONE tool call
the whole time: uninterruptible (the interrupt flag is only checked
between loop iterations) and serial (a batch of manual runs executed
one by one). A Telegram session that kicked off dozens of new jobs
'right now' was wedged for hours ignoring every interrupt.

action='run' now rides the async-delegation rail delegate_task
background mode uses: the at-most-once claim is taken synchronously
(so paused/missing/already-firing jobs still report immediately),
the run executes on the shared daemon executor, the tool returns at
once with a delegation handle, and the job's outcome re-enters the
conversation as a type='async_delegation' completion event through
the existing completion-queue drains (CLI + gateway) — preserving
message-role alternation and the prompt cache.

Sync fallbacks preserved:
- no routable session (direct Python callers, hermes cron run)
- async delivery unsupported (hermes -z, cron child sessions,
  Kanban workers, stateless HTTP)
- dispatch pool at capacity (claim already taken — runs inline
  rather than stranding it)

The completion block reports ok/failure, delivery target, next
scheduled run, and an excerpt of the job's saved output.
Salvaged from PR NousResearch#53395 by @izumi0uu: the fire claim's 300s TTL is
routinely outlived by real cron jobs, so claim_job_for_fire alone
cannot stop a manual cronjob(action='run') from double-firing a job
the ticker (or another manual run) is still executing.

Extract the ticker's _submit_with_guard running-set check into shared
module-level helpers (try_register_running_job / release_running_job)
and register manual runs through the same set — one dedupe owner, no
drift. Manual runs also become visible to get_running_job_ids (the
gateway shutdown drain, NousResearch#60432) and mark_running_jobs_interrupted,
which previously could not see them.

The background dispatch path pre-checks the running set so a mid-run
job reports 'already running' in the tool response immediately
instead of as a delayed error completion event; the authoritative
atomic check remains in _run_claimed_job on the worker.

Co-authored-by: izumi0uu <izumi0uu@gmail.com>
Pins the scheduler-boundary contract: extra_prompt is appended under
'## Run Context', does not mutate job['prompt'], and the header is
absent when extra_prompt is omitted.

Addresses review feedback from harjothkhara on PR NousResearch#57342.
…esearch#57331)

Salvaged from PR NousResearch#57342 by @liuhao1024 (with the injection-scan half
from PR NousResearch#57360 by @ghedeselmabot): cronjob(action='run', prompt=...)
silently discarded the prompt argument — per-run context never
reached the spawned cron session.

The prompt is now threaded as extra_prompt through the whole chain
(cronjob run action → _try_dispatch_background_run/_execute_job_now →
_run_claimed_job → run_one_job → run_job → _build_job_prompt) and
appended to the stored prompt under a '## Run Context' header for
that single fire only — never persisted to the job definition. It
passes the same strict _scan_cron_prompt injection scan as stored
prompts before firing, and works identically on the background and
sync fallback paths.

Test fakes across tests/cron/ updated to accept the new kwargs
(sibling-test blast radius from the signature change).

Co-authored-by: liuhao1024 <liuhao1024@users.noreply.github.com>
… standalone send

Fixes NousResearch#61495

When manually triggering cron jobs from a live Matrix session, delivery
would fail with "Timeout context manager should be used inside a task"
because the aiohttp.ClientTimeout context manager requires a proper asyncio
task context.

Use asyncio.wait_for() instead of aiohttp.ClientTimeout to avoid this error,
following the same pattern as the Weixin platform (gateway/platforms/weixin.py).

Changes:
- Remove aiohttp.ClientTimeout(total=30) from ClientSession constructor
- Wrap the send operation in a nested async function (_do_send)
- Use asyncio.wait_for(_do_send(), timeout=30) for timeout handling
- Catch asyncio.TimeoutError explicitly and return clear error message
Covers the behavior shipped in NousResearch#80807 (background dispatch for
cronjob action='run') and NousResearch#80838 (per-run '## Run Context' prompt,
gateway-loop delivery): immediate return with handle, completion
re-entering the conversation, in-flight dedupe, transient context
injection with prompt scanning, and the sync fallbacks.
…open fix)

Adversarial review of the salvaged recovery found a reachable fail-open:
compression continuations inherit the rotated agent's model_config
verbatim (publish_compression_child callers pass
agent._session_init_model_config), so a delegate subagent's continuation
carries _delegate_from=<the delegate's own parent>. The marker-PRESENCE
filters in reopen_orphaned_compression_session and
find_live_compression_child misclassified such a REAL continuation as a
delegate child:

- reopen: parent 'orphaned' -> reopened while a live continuation exists
  -> two live heads in one lineage (verified with a live repro)
- find_live: adoption misses the continuation (fail-closed, masked the
  fork pre-PR; the PR made it active)

Fix: markers only disqualify a child when they point at the queried
parent (shared _NON_CONTINUATION_CHILD_FILTER_SQL fragment, also
resolving the duplicated-SQL drift risk flagged by the reuse reviewer).
Both directions regression-tested: reopen fails closed on an
inherited-marker continuation; find_live adopts it.

Also from review: reopen-failure log raised debug->warning (the failure
hard-fails the turn moments later), commit-semantics hardening comment
on the lease DELETE path, blank-line nit.

The three read-only projection walks (get_compression_tip,
list_sessions_rich chain, resume walk) share the marker-presence shape
but fail closed (skip a continuation -> resume shows the parent), and
the fixed adoption path self-heals that case at turn start; left as-is.
Platform registry create_adapter() gates on check_fn before the adapter
exists, so wiring the passive probe permanently blocked connect() and
the existing check_teams_requirements() lazy-install never ran.
Step 5 only showed docker compose from a clone; native/systemd users
hit missing compose files and PEP 668 system-pip failures.
…ive installer)

PlatformEntry.check_fn served three contradictory roles: adapter-creation
gate, config auto-enablement gate, and status display. Plugins had to pick
one function for all three:

- Active installer as check_fn (discord/slack/telegram/matrix/dingtalk/
  feishu): every status display could pip-install SDKs as a side effect
  (the desktop 94% boot-loop class).
- Passive probe as check_fn (teams, wecom_callback): create_adapter()
  returned None before connect() could lazy-install, so the SDK never
  installed (NousResearch#79812 deadlock; wecom_callback's platform.wecom_callback
  LAZY_DEPS entry was dead code).

The split makes both call sites correct by construction:

- check_fn is now contractually PASSIVE (probe only, never installs).
- New optional PlatformEntry.ensure_deps_fn is the ACTIVE installer;
  create_adapter() runs it exactly when check_fn is False — the platform
  is enabled+configured and the gateway is about to connect it.
- Config enablement keeps a configured platform whose deps are missing
  but installable; the install itself is deferred to create_adapter().
- Status surfaces (_platform_status, hermes status) read only the
  passive probe and can never trigger pip.

Migrated all lazy-installable platform plugins to the split; platforms
with no optional deps (irc/ntfy/buzz/simplex/line/a2a/...) are unchanged
— no ensure_deps_fn means a False check_fn stays a hard block.
wecom_callback gains a working installer for the first time.

Builds on @xxxigm's NousResearch#79812 (both commits cherry-picked with authorship
preserved), reworking the check_fn swap into the two-field split so the
Teams fix doesn't reintroduce install-on-status.
- gateway/config.py: rewrite the stale enablement-pass header comment that
  still described check_fn as 'the single source of truth for are-my-env-
  vars-set' / 'lazy-installs it' — both false under the new contract.
- teams: check_requirements docstring wrongly claimed credential checks
  (body checks only SDK/aiohttp presence); derive install_hint from the
  canonical LAZY_DEPS pins + sys.executable instead of hardcoding
  '~/.hermes/hermes-agent/venv/bin/pip' and version pins (wrong under
  HERMES_HOME overrides / profile installs; pins go stale on CVE bumps);
  connect() fatal-error hints now point at the venv pip instead of bare
  system pip (the PEP 668 trap the docs warn about).
- teams docs: drop exact version pins from the two manual-install commands
  (LAZY_DEPS is the source of truth; unpinned installs still work and the
  text can't go stale).
- hermes_cli/status.py: per-entry exception guard around check_fn so one
  raising probe can't abort the listing of all remaining plugin platforms
  (aligns with the other three call sites).
- tests: rename test_register_check_fn_is_active_lazy_installer ->
  test_register_splits_passive_probe_from_active_installer (name said the
  opposite of what it verifies).
- matrix/dingtalk: extract deps-only installers (ensure_matrix_deps,
  ensure_dingtalk_deps) and register THOSE as ensure_deps_fn — the prior
  check_*_requirements combined credential env checks with the install,
  so a platform configured via PlatformConfig.extra (which is_connected
  accepts) would pass enablement, reach create_adapter(), and have the
  'installer' veto on env-var grounds before installing anything —
  re-creating the NousResearch#79812 deadlock for extra-configured setups.  The
  combined deps+credentials functions remain for setup/status callers.
- matrix/feishu passive probes: use the existing lazy_deps.is_available()
  instead of hand-rolling 'not feature_missing(...)' (reuse finding).
- teams: module docstring no longer recommends bare system pip (the
  PEP 668 trap purged everywhere else); docs troubleshooting row updated
  to match the new hint text.
- wecom_callback: drop dead 'global ET, DEFUSEDXML_AVAILABLE'
  (ensure_and_bind mutates the module dict directly; nothing assigns).
- tests: parametrized wiring contract for all 8 lazy-installable
  platforms — ensure_deps_fn present and distinct from check_fn
  (behavior contract, not identity snapshot, so renames don't churn it).
…pip=True)

Fold the remaining simplify-code reuse finding: teams' _install_hint()
duplicated lazy_deps' spec-fetch + quote + join (feature_install_command
already builds pip commands from LAZY_DEPS). Add a venv_pip=True variant
to feature_install_command — sys.executable -m pip targeting, correct in
every install layout and immune to PEP 668 — and shrink the teams helper
to a one-line call.  Also gives matrix and the other platforms a shared
derived hint to adopt later.  New test mutation-checked (fails when
venv_pip returns the uv form).
@vashkartik

vashkartik commented Aug 10, 2026

Copy link
Copy Markdown
Owner Author

Closing this automation PR as a stale prepared candidate, not as a rejected upstream sync. The protected baseline is still 4371712 while current upstream is 03fa32c (907 baseline-aware commits ahead). The 2026-08-10 04:00 preparation failed before publication on six unresolved files: contributors/emails/StanleyStetson@users.noreply.github.com, gateway/platforms/api_server.py, hermes_cli/web_routers/sessions.py, hermes_state.py, tests/test_tui_gateway_server.py, and tui_gateway/methods_prompt.py. Keeping this old green PR open would misrepresent current sync health. Head 2be2098 and the reusable automation branch remain preserved; no live apply or release was performed.

@vashkartik vashkartik closed this Aug 10, 2026
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.