Skip to content

sync: merge upstream/main — 301 commits (2026-06-21), 23 conflicts resolved#405

Merged
Lexus2016 merged 305 commits into
mainfrom
sync/upstream-2026-06-21
Jun 21, 2026
Merged

sync: merge upstream/main — 301 commits (2026-06-21), 23 conflicts resolved#405
Lexus2016 merged 305 commits into
mainfrom
sync/upstream-2026-06-21

Conversation

@Lexus2016

Copy link
Copy Markdown
Owner

Closes #403

Why

The autonomous upstream-sync stalled on 2026-06-19 (PR #352 was the last successful sync). It uses an all-or-nothing threshold (auto-merge only if ≤~80 behind AND ≤10 conflicts, none in core) — the first day a core conflict appeared it escalated (#403) and never progressed, so the fork fell to 301 commits behind, missing ~25 apps/desktop updates (composer popout, tooltips, audio-mute fixes — what the owner noticed).

What

Merges upstream/main (301 commits). 23 conflict hunks across 12 files resolved authorship-first (upstream-domain logic → upstream; our evolution additions kept additive). Opened for owner review — do NOT squash; review the conflict resolutions below before merging.

Conflict resolutions & evolution features preserved

File Resolution
cron/jobs.py Kept our mark_job_started/recover_interrupted_jobs (#105); migrated them onto upstream's new _jobs_lock() (multi-process file lock).
cron/scheduler.py Adopted upstream's extracted run_one_job; grafted our mark_job_started (#105) + strip_reasoning_for_delivery into it. Cron-script env now passes our profile run_env through _sanitize_subprocess_env (upstream SECURITY.md §2.3) — verified our no_agent scripts don't need provider env.
agent/chat_completion_helpers.py Kept our try_activate_fallback(…, api_error=…); added upstream's rewrite_prompt_model_identity (body already calls it); took env_float.
run_agent.py Took upstream create_anthropic_message (subsumes our sanitize+create), _session_source_for_agent, env_float, background policy (_is_subagent) — kept our handoff_mode.
tools/delegate_tool.py Took upstream _model_background_value — kept our handoff_mode.
tools/mcp_tool.py Took upstream __slots__ superset (+_elicitation/_pending_call_context/_ping_unsupported) and _get_secret (env-fallback).
tools/cronjob_tools.py Kept both _reset_cron_failure (ours) + _notify_provider_jobs_changed_safe (upstream) at all 5 sites.
agent/turn_retry_state.py (+test) Kept both fail_fast_attempted (ours) + auth_failover_attempted (upstream).
nix/lib.nix Took upstream importNpmLock (removes our npmDepsHash sync churn).
scripts/release.py Unioned AUTHOR_MAP. README.md: kept fork identity.

Testing

  • All 12 conflicted python files compile; compileall across agent/cron/tools/scripts/hermes_cli clean.
  • tests/cron 493+ pass. Our test_script_reads_env_from_hermes_dotenv (adapted to the new env policy) + upstream's test_script_subprocess_env_sanitized both pass. Our suites (prompt_builder, self_critique, security_guidance, turn_retry_state) green (212 passed).
  • Local env lacks croniter/prompt_toolkit/signal-delivery → some tests/cron/tests/run_agent cases can't run locally; full verification is this PR's CI.

Follow-up (separate)

Make upstream-sync incremental (merge to the last conflict-free point each day) so one far conflict can't freeze it again, and have the watchdog alert when sync is stuck N days. Tracked for a future change.

teknium1 and others added 30 commits June 19, 2026 06:31
… clean text

The Discord fix (previous commit) handles dict-shaped clarify choices at the
Discord adapter only. The same dict-repr leak originates upstream at
tools/clarify_tool.py's str(c).strip() normalization — the single
platform-agnostic point both the CLI and every gateway adapter flow through.

When an LLM emits [{"description": "..."}] instead of bare strings, str(c)
produced {'description': '...'} which leaked onto the CLI panel
(cli.py:13048/13081), was returned verbatim as the user's answer
(cli.py:11945), and hit Telegram's numbered list too.

Add _flatten_choice (same label->description->text->title unwrap as the
Discord adapter, name/value excluded, keyless dicts dropped) and apply it at
the normalization line. Fixes CLI + Telegram + all platforms at the root;
the Discord smart-truncation now operates on already-clean text.

Adds johnjacobkenny to AUTHOR_MAP for the salvaged commit.
When a gateway agent is reused from cache, it retains the max_iterations
from its initial creation. If config.yaml agent.max_turns or HERMES_MAX_ITERATIONS
changed between turns, the cached agent's budget becomes stale.

Before reusing a cached agent, refresh agent.max_iterations from the
freshly-resolved value (read from env/config at line 14585).

Fixes partial issue from PR #48127: handles fresh agent creation + cached agent reuse.
Replaces the tautological test from the original PR (which asserted a
plain assignment it performed itself in the test body) with one that
exercises the actual contracts: _init_cached_agent_for_turn leaves
max_iterations untouched, and the per-turn IterationBudget rebuild
(turn_context.py) propagates a refreshed cap.
`hermes gateway restart` on Windows could take the gateway offline with no
replacement. restart() was stop() -> sleep(1.0) -> start(), but the graceful
drain can run up to ~180s while the detached pythonw process stays alive. The
1s sleep let start() run against the still-draining old process; its
"already running" guard then no-opped, and when the old process finally exited
nothing relaunched it.

Two root causes, both fixed:

1. Loose PID detection. `_scan_gateway_pids` and the gateway.status helpers
   used substring matches ("... gateway" in cmdline) for lifecycle decisions,
   so they false-matched `gateway status`/`dashboard` siblings and unrelated
   processes like `python -m tui_gateway`, plus stale gateway.pid records.
   Add a shared strict matcher `looks_like_gateway_command_line()` in
   gateway/status.py that requires the real `gateway run` subcommand (or the
   dedicated entrypoints), and route `_looks_like_gateway_process`,
   `_record_looks_like_gateway`, and `_scan_gateway_pids` through it.

2. restart() race. Wait until the gateway is authoritatively gone
   (`get_running_pid()` + strict `_gateway_pids()`) before relaunch; force-kill
   once if it lingers and raise rather than start a duplicate; verify the
   relaunch produced a running gateway and raise loudly if not (no more
   exit-0 silent outage).

Scoped to Windows; systemd/launchd restart paths are already drain-aware.
Adds tests/gateway/test_gateway_command_line_matcher.py.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The pyproject addopts pin `--timeout-method=signal` relies on signal.SIGALRM,
which doesn't exist on Windows. pytest-timeout raised AttributeError at timer
setup and aborted the entire run before any test executed, so the suite was
unrunnable on Windows by default. Override timeout_method to "thread" on
Windows in pytest_configure; POSIX keeps the more reliable signal method.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Address correctness gaps found in pre-PR review of the strict matcher:

- Profile selectors can appear on EITHER side of the `gateway` token
  (`_apply_profile_override` strips `--profile`/`-p` from anywhere in argv
  before argparse), so `hermes gateway --profile work run` and
  `python -m hermes_cli.main gateway -p work run` are valid launches the
  previous matcher wrongly rejected. Strip `--profile`/`-p`/`--profile=`/`-p=`
  from anywhere before locating the subcommand.
- A profile literally named `gateway` (`hermes -p gateway gateway run`) made
  the old token scan stop on the profile value; stripping the selector+value
  first fixes it.
- Tokenize quote-aware with `shlex` so quoted Windows paths containing spaces
  (`"C:\Program Files\Hermes\hermes-gateway.exe"`) are no longer split mid-path
  and the dedicated-entrypoint match survives.

Without these, the matcher could MISS a real running gateway -> the opposite
failure (restart/status reporting "down" when up). Adds regression tests for
all three shapes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…tecture-diagram + concept-diagrams (#48899)"

This reverts commit 9362ce2.
…#49063)

System messages (/debug, /status, etc.) were not in the desktop app's
text-selection allowlist, so log output in the thread could not be copied.
/update calls dieWithCode(42) which tears down the gateway and
hard-exits the Node process — the same PTY-killing path that /exit
and /quit use.  In the hosted dashboard chat there is no Python
update wrapper to catch exit code 42, and the PTY death bricks the
tab until a browser refresh.

Mirror the DASHBOARD_TUI_MODE guard that #48882 added for /exit and
/quit: refuse early with an explanatory message.
The built-in Piper provider (tts.provider: piper, Python piper-tts
package) already constructs piper.SynthesisConfig for the advanced
tuning knobs, but did not forward speaker_id from the user config.

This wires tts.piper.speaker_id through to SynthesisConfig.speaker_id
so multi-speaker ONNX models (e.g. libritts_r) can be addressed via
config without dropping to the command-provider path.

Changes:
- Add speaker_id to the has_advanced tuple so setting it triggers
  SynthesisConfig construction (same gating as the other knobs).
- Pass speaker_id=speaker_id to SynthesisConfig. Defaults to 0
  (Piper's own default; single-speaker models ignore the field).
- Tolerant parse: bad input (non-int strings, lists, dicts) is
  dropped to 0 instead of raising. Booleans are rejected outright
  (True/False would silently coerce to 1/0 and hide a config
  mistake). Mirrors the same shape as the command-provider's
  _resolve_command_tts_optional_number helper.

speaker_id is applied per-call via syn_config.speaker_id, so the
PiperVoice cache key is intentionally left as just (model, cuda) --
the same loaded model serves all speakers. Tests cover the
config knob, the tolerant parse, and the no-reload invariant.

sentence_silence is intentionally not added here: the Python
piper-tts SynthesisConfig does not expose that field (CLI-only).
…ebug share

gui.log was registered in hermes_cli/logs.py::LOG_FILES (and surfaced by
`hermes logs gui`) but was never wired into `hermes debug share`. The share
report captured agent/errors/gateway/desktop tails plus full agent/gateway/
desktop logs — but nothing from gui.log, the surface the dashboard, TUI-over-
PTY bridge, and websocket layer (hermes_cli.web_server / pty_bridge /
tui_gateway) actually write to. A user reporting a dashboard or TUI bug shared
zero breadcrumbs from the broken surface.

Wire gui.log through all three share surfaces, matching the existing pattern:
- _capture_default_log_snapshots(): capture the gui snapshot (redacted like the rest)
- collect_debug_report(): add the gui.log summary tail block
- build_debug_share(): pull gui full_text, prepend dump header + redaction banner, add to the upload loop
- run_debug_share() --local branch: same, plus the local print block
- _PRIVACY_NOTICE: name gui.log in both bullets

Redaction is inherited for free — the gui snapshot goes through the same
_capture_log_snapshot(..., redact=redact) path, so secrets are scrubbed in
both the tail and full text (verified E2E: seeded key masked by default,
passes through under --no-redact, raw token never leaks).

Tests: seed gui.log in the fixture, add test_report_includes_gui_log, and bump
the upload-count tripwire 4->5 (test_share_uploads_five_pastes).
Commit 6724daa added refresh_interval=1.0 to keep the idle clock
ticking, but unconditional 1 Hz redraws in non-fullscreen prompt_toolkit
mode cause terminal emulators (Xshell, iTerm2, Windows Terminal) to
auto-scroll to the bottom on every tick — breaking scroll-up to read
history.

Drive it from display.cli_refresh_interval (0 = disabled, the default)
so users who want the ticking clock can opt in without affecting everyone.

Fixes: #48309
Related: 6724daa, 8972a15
Follow-up to the salvaged #48312 — adds the config-default test (ported
from #48319) and the AUTHOR_MAP entry for the cherry-picked commit.
A plain /model <name> switch only lasted for the current session — every
new session reverted to the previously-configured model, so users had to
re-switch every time (e.g. glm-5.1 -> glm-5.2 on every launch).

Persist-by-default is now the behavior across all three /model surfaces
(CLI, gateway, TUI/dashboard), gated by a new config key
model.persist_switch_by_default (default true):

  /model <name>             switch model (persists to config.yaml)
  /model <name> --session   switch for this session only
  /model <name> --global    switch and persist (explicit, unchanged)

The effective persistence is resolved once via resolve_persist_behavior()
in hermes_cli/model_switch.py so --session opts out, --global opts in,
and the config-gated default applies otherwise. --global remains a valid
explicit no-op alias for the new default.
Mirrors the existing Gemini TTS audio-tag rewrite path. When the input
has no explicit user/model speech tags, ask the configured auxiliary
model to insert a richer set of xAI-supported tags (laughs, sighs,
whispers, soft/loud, slow/fast, etc.) so voice-mode replies sound more
expressive. Falls back to the local conservative [pause]-only transform
on any auxiliary-model failure.
The previous xAI auto-speech-tag tests asserted on the local
pause-only fallback and only passed because call_llm silently
returns None in the test environment. They gave zero coverage of
the new auxiliary-rewrite path added in the previous commit.

Add tests that:
- mock agent.auxiliary_client.call_llm and pin down the new contract
  (auxiliary rewriter output wins over the local fallback)
- verify the system prompt lists every documented inline + wrapping
  tag and uses BBCode-style [/tag] closing syntax
- cover markdown-fence stripping (with and without language hint)
- exercise the local fallback on rewriter exception, empty response,
  None response, and missing-choices response
- confirm call_llm is NOT invoked when the input already has
  explicit speech tags, or is empty / whitespace-only
- replace the end-to-end test that asserted on the silent-fallback
  output with one that mocks the rewriter and asserts the
  rewriter's tagged text is what reaches the xAI TTS API
…top tabs

Adds hermes_cli/provider_catalog.py, deriving one descriptor per provider from
the CANONICAL_PROVIDERS universe (what `hermes model` renders, auto-extended
from provider plugins), joined with auth/env from PROVIDER_REGISTRY and display
metadata from ProviderProfile (with canonical/env fallbacks for the four
profile-less providers and the many profiles with blank display/signup fields).

Each descriptor is tagged with the desktop tab it belongs on (keys vs accounts)
by auth_type. This is the single source of truth the desktop Providers tabs will
derive membership from, so they can no longer drift from the CLI picker.

Tests assert the parity contract (catalog == hermes model universe) and tab
routing as invariants, not snapshots.
…catalog

The Keys tab now surfaces every keys-tab provider in provider_catalog() (the
`hermes model` universe), synthesizing a card even when the env var has no hand
entry in OPTIONAL_ENV_VARS. Closes the drift where openai-api, kilocode, novita,
tencent-tokenhub, and copilot were CLI-configurable but invisible in the desktop
Providers → API keys tab.

Each provider row now carries backend-derived provider/provider_label grouping
hints so the desktop can group by the same provider identity the CLI picker
uses. Hand OPTIONAL_ENV_VARS prose still wins where present (enrichment, not a
gate). Shared non-provider credentials (e.g. tool-category GITHUB_TOKEN) are
explicitly not hijacked into a provider card — Copilot uses its provider-owned
COPILOT_GITHUB_TOKEN.
…catalog

/api/providers/oauth now unions the explicit hand-tuned OAuth cards
(_OAUTH_PROVIDER_CATALOG — bespoke flow/status/cli, plus the api-key Anthropic
PKCE card and synthetic claude-code row) with every accounts-tab provider in
provider_catalog(). Any OAuth/external provider in the `hermes model` universe
now appears automatically, closing the drift where google-gemini-cli and
copilot-acp had no Accounts card despite being CLI-configurable.

Adds read-only status cards for google-gemini-cli (via existing
get_gemini_oauth_auth_status) and copilot-acp (managed-by-CLI, like claude-code).
DELETE handler routes through the same _build_oauth_catalog() builder.

Parity test asserts the Accounts tab offers every accounts-tab catalog provider
as an invariant.
buildProviderKeyGroups now groups provider env vars by the backend-supplied
provider/provider_label (from the unified catalog — the same identity hermes
model uses), falling back to the desktop PROVIDER_GROUPS prefix match only when
the backend gives no hint. A provider the backend tags now always renders its
own Keys card, even with no hand-maintained PROVIDER_GROUPS prefix row —
PROVIDER_GROUPS is demoted to a presentation overlay (priority/blurb/docs).

Adds provider/provider_label to EnvVarInfo. New vitest asserts a backend-tagged
provider with no prefix row still renders a card.
Adds the end-to-end parity contract test: every CANONICAL_PROVIDERS entry (the
`hermes model` universe) must be configurable on a desktop Providers tab —
keys(/api/env) ∪ ids(/api/providers/oauth) ⊇ canonical. Asserted as an
invariant against the live endpoints so the GUI can never silently drift from
the CLI again.

Surfacing this contract caught Bedrock: it's aws_sdk (no api-key vars), so it
had no Keys card. /api/env now tags AWS_REGION/AWS_PROFILE to the bedrock
provider card. Anthropic is whitelisted as a legitimate dual-tab provider
(direct API key + subscription OAuth).

Also refreshes the _OAUTH_PROVIDER_CATALOG docstring to describe its new role
as the override base for _build_oauth_catalog().
- API-keys tab: a SearchField filters provider cards by name / env-var key /
  description, with a 'no providers match' empty state. Card order stays
  priority-then-name (curated PROVIDER_GROUPS priority floats recommended
  providers up; equal priority falls back to alphabetical).
- Accounts tab: 'Other providers' keep sortProviders order (priority, then
  name) — unchanged.

Adds searchKeys/noKeysMatch i18n strings across all four locales. Vitest covers
priority/name ordering + live filtering + empty state.
Address review feedback on the keyVar test helper: it mocks one /api/env row
(an EnvVarInfo), so type it as such and mirror the sibling provider() factory's
base-plus-Partial-override shape instead of hardcoding positional args and
fabricated fields (description='X direct API', url=''). Route the WidgetAI test
through it too, removing the inline duplicate of the same object shape.
teknium1 and others added 22 commits June 20, 2026 23:23
…docstring

The MCP discovery wait is now bounded by the config-driven mcp_discovery_timeout
(default 1.5s), not the old 0.75s flat value. Updates the _schedule_mcp_late_refresh
docstring that still cited ~0.75s after #49208 made the bound configurable.
The shell-hook stdin payload's extra object contains event-specific
kwargs, but the docstring only mentioned the field without listing
what each event actually puts inside it.

Add a reference table covering post_tool_call, pre_tool_call,
on_session_start, on_session_end, and subagent_stop — the five
hook sites that emit extra keys beyond the top-level payload.

Closes #49370
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Brings the antigravity-cli skill to parity with the codex / claude-code
delegation playbooks. Additive only — auth/sandbox/plugin/settings content
is unchanged.

- New 'Delegation patterns' section: one-shot, background bounded runs,
  interactive PTY+tmux, parallel worktree fan-out, and an orchestration
  boundary note (agy is a worker backend / reviewer, not a coordination
  primitive).
- Documents the two ways agy -p differs from claude-code: plain-text
  output (no --output-format json / result envelope) and bounding via
  --print-timeout rather than a nonexistent --max-turns. Mirrored into
  Pitfalls.
- Bumps version 0.1.0 -> 0.2.0.
The MCP section pointed to docs/mcp.md, which does not exist. Point it
to website/docs/user-guide/features/mcp.md, matching the existing
hooks.md reference convention in the same file.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Telegram DOES echo a rich message's content back in
reply_to_message.api_kwargs['rich_message']['blocks'] when a user
replies to it. Read that native field first in _build_message_event,
keeping the local send-time index only as a fallback. Duck-type
api_kwargs via .get() since it is a mappingproxy, not a dict.

Fixes #49534
The native echo recovery handles replies to most rich messages, but
messages sent before the bot's first rich send have no echo to read.
record() was only called on the fresh-send path (_try_send_rich); a
streamed final finalized via _try_edit_rich/editMessageText was never
indexed, so a reply to it had neither a native echo nor an index entry.
Mirror the fresh-send record() into the edit success path to close
that gap.
…e (#49997)

Single-op replace/remove failed with a dead-end 'old_text is required'
error when a structured-output client omitted the optional old_text field
(it can't be schema-required without a top-level if/then combinator that
OpenAI's Codex backend 400s on). The model couldn't recover.

Now a missing old_text returns the current entry inventory plus a retry
instruction (mirroring the batch path's _batch_error), so the model can
reissue the call with old_text set. Also sharpens the old_text schema
description to state it's required for replace/remove.

Fixes #49466, #43412.
…e (#11996)

protect_first_n keeps the first N non-system messages verbatim through
compaction so the original task framing survives. But it was applied on
EVERY compression pass: the same early user turns were re-copied into each
child session and never summarized away, so across a long, repeatedly-
compressed session those old messages became immortal and grew the
protected head unboundedly (#11996, P1).

Decay it: protect_first_n applies on the FIRST compaction only. Once the
session has been compressed at least once (compression_count >= 1, or a
handoff summary already exists), the early turns are captured in the
summary, so _effective_protect_first_n() returns 0 and only the system
prompt stays protected. The decay is read at compress_start computation
time, before compression_count/_previous_summary are mutated at the end of
compress(), so the first pass still protects correctly.

Co-authored-by: truenorth-lj <liliangjya@gmail.com>
Co-authored-by: davidvv <david.vv@icloud.com>
Cron jobs created without an explicit `model` are stored as `model: null`.
At fire time `run_job` resolved `model = job.get("model") or os.getenv(
"HERMES_MODEL") or ""` and then `_model_cfg.get("default", model)`, so when
config.yaml had no `model.default` (or `model: {default: null}`) an empty
string flowed straight to the provider and surfaced as an opaque HTTP 400
("Model parameter is required" / "model: String should have at least 1
character"). The operator had to inspect jobs.json to discover the job was
stored with a null model.

This change makes cron model resolution robust and symmetric with the CLI:

- Coerce `model: null`/missing config to `{}` so a falsy default never
  overwrites an already-resolved env value with `None`.
- Only overwrite `model` from `model.default` when the resolved value is
  truthy; accept a `model.model` alias key, mirroring the sibling resolvers
  in hermes_cli/oneshot.py, fallback_cmd.py and prompt_size.py.
- Resolve AFTER the managed-scope overlay so an administrator-pinned model
  still wins.
- Fail fast with an actionable error (caught by run_job's outer handler and
  recorded as the job's last_error — the cron ticker is unaffected) instead
  of letting an empty model reach the API.
- The per-job model is re-read every tick, so a `cronjob action=update
  model=...` after a failed run takes effect on the next tick (no cache).

Adds tests/cron/conftest.py pinning a default HERMES_MODEL so existing
run_job tests don't trip the new guard, plus regression tests covering env
fallback, config.default fallback, string-form config, the model alias key,
null-default-no-clobber, corrupt-config graceful degradation, fail-fast,
and the no-cache re-read property.

Salvaged from #24005, rebased onto current main, with additional test
coverage folded in from #45550 and the alias-key behavior from #43952.

Fixes #43899
Fixes #23979
Fixes #22761

Co-authored-by: szzhoujiarui-sketch <szzhoujiarui@gmail.com>
Co-authored-by: rayjun <rayjun0412@gmail.com>
Salvage co-authors of the cron model.default fix.
The in-process cron ticker (cron/scheduler_provider.py) caught only
`Exception` and logged at DEBUG, so a `SystemExit`/`KeyboardInterrupt`
raised from a misbehaving provider SDK or agent retry path killed the
ticker thread silently. The gateway PROCESS stayed up, so `hermes cron
status` — which only checks `find_gateway_pids()` — kept reporting
"✓ jobs will fire automatically" while no jobs ever fired (#32612,
#32895).

This makes ticker death survivable and detectable:

- The ticker loop now catches `BaseException` and logs at ERROR with a
  traceback, so a single bad tick no longer tears the thread down and
  the failure is visible in the gateway log.
- The loop records a heartbeat (`cron/ticker_heartbeat`, epoch seconds)
  on startup and after every tick — best-effort, never raised into the
  loop. Both ticker entry points (the gateway and the desktop fallback
  in web_server.py) funnel through `InProcessCronScheduler.start`, so one
  heartbeat site covers both.
- `hermes cron status` now reads the heartbeat age: if the gateway is
  running but the heartbeat is stale (> 200s, i.e. several missed ~60s
  ticks), it reports the ticker as STALLED and suggests a restart instead
  of falsely claiming jobs will fire. A missing heartbeat (older build /
  never ran) is treated as "unknown", not "dead".

Adds tests for BaseException survival, per-iteration heartbeat recording,
heartbeat round-trip/age, staleness detection, and silent-write-failure.

Salvaged from #49660 (BaseException survival on current structure),
extended with the heartbeat + honest-status reporting that the earlier
(pre-refactor) watchdog PRs #35616 and #33849 proposed.

Fixes #32612
Fixes #32895

Co-authored-by: banditburai <promptsiren@gmail.com>
Co-authored-by: sweetcornna <96944678+sweetcornna@users.noreply.github.com>
Salvage co-author of the cron ticker-liveness fix.
Catch up the fork to nousresearch/hermes-agent:main after the autonomous
upstream-sync stalled on 2026-06-19 (all-or-nothing threshold froze on the
first core conflict; see audit). Brings 301 upstream commits incl. ~25
apps/desktop updates (composer popout, tooltips, audio-mute fixes).

Resolved 23 conflict hunks across 12 files, authorship-first (upstream-domain
logic → upstream; our evolution additions kept additive). Evolution features
explicitly preserved through the merge:
- cron/jobs.py: kept mark_job_started/recover_interrupted_jobs (#105),
  migrated them onto upstream's new _jobs_lock() (multi-process file lock).
- cron/scheduler.py: adopted upstream's extracted run_one_job + grafted our
  mark_job_started (#105) and strip_reasoning_for_delivery into it; passed
  our profile-built run_env through upstream's _sanitize_subprocess_env.
- agent/chat_completion_helpers.py: kept our try_activate_fallback(api_error)
  signature + body; added upstream's rewrite_prompt_model_identity (body
  already calls it); took env_float helper.
- run_agent.py / tools/delegate_tool.py: took upstream's background policy
  (_is_subagent / _model_background_value) while keeping our handoff_mode;
  took create_anthropic_message (subsumes our sanitize+create), env_float,
  _session_source_for_agent.
- tools/mcp_tool.py: took upstream __slots__ superset (+_elicitation,
  _pending_call_context, _ping_unsupported) and _get_secret (env-fallback).
- tools/cronjob_tools.py: kept both _reset_cron_failure (ours) and
  _notify_provider_jobs_changed_safe (upstream) at all 5 sites.
- agent/turn_retry_state.py + test: kept both fail_fast_attempted (ours) and
  auth_failover_attempted (upstream).
- nix/lib.nix: took upstream importNpmLock (removes our npmDepsHash churn).
- scripts/release.py: unioned AUTHOR_MAP. README: kept fork identity.
…2.3)

Post-merge fix for the cron/scheduler.py env conflict. Upstream hardened
no_agent cron-script subprocess env to strip provider secrets via
_sanitize_subprocess_env (_HERMES_PROVIDER_ENV_BLOCKLIST). Resolution:
- Pass our profile-built run_env (HERMES_HOME, profile HOME, non-provider
  .env config) THROUGH _sanitize_subprocess_env — keeps our profile setup
  AND satisfies upstream's test_script_subprocess_env_sanitized.
- Verified zero blast radius: our only no_agent scripts (evolution_watchdog,
  evolution_funnel) do not read provider keys from env.
- Adapt our test_script_reads_env_from_hermes_dotenv to assert NON-provider
  .env config still reaches scripts (provider-secret stripping is covered by
  upstream's test).
@github-actions

github-actions Bot commented Jun 21, 2026

Copy link
Copy Markdown
Contributor

🔎 Lint report: sync/upstream-2026-06-21 vs origin/main

ruff

Total: 0 on HEAD, 0 on base (➖ 0)

🆕 New issues: none

✅ Fixed issues: none

Unchanged: 0 pre-existing issues carried over.

ty (type checker)

Total: 11556 on HEAD, 11322 on base (🆕 +234)

🆕 New issues (370):

Rule Count
unresolved-attribute 132
unresolved-import 119
invalid-argument-type 30
not-subscriptable 23
invalid-assignment 17
invalid-method-override 17
unsupported-operator 15
unresolved-reference 5
call-non-callable 3
invalid-return-type 3
unused-type-ignore-comment 3
no-matching-overload 2
invalid-raise 1
First entries
tests/hermes_cli/test_config.py:966: [invalid-argument-type] invalid-argument-type: Method `__getitem__` of type `bound method str.__getitem__(key: SupportsIndex | slice[SupportsIndex | None, SupportsIndex | None, SupportsIndex | None], /) -> str` cannot be called with key of type `Literal["cli_refresh_interval"]` on object of type `str`
plugins/platforms/wecom/callback_adapter.py:141: [unresolved-attribute] unresolved-attribute: Attribute `TCPSite` is not defined on `None` in union `Unknown | None`
plugins/platforms/telegram/adapter.py:5060: [unresolved-attribute] unresolved-attribute: Attribute `CHANNEL` is not defined on `None` in union `Unknown | None`
plugins/platforms/telegram/adapter.py:2122: [unresolved-attribute] unresolved-attribute: Attribute `Document` is not defined on `None` in union `Unknown | None`
tests/plugins/test_raft_check_fn_silent.py:16: [unresolved-import] unresolved-import: Cannot resolve imported module `pytest`
tests/agent/test_compression_rotation_state.py:56: [unresolved-attribute] unresolved-attribute: Unresolved attribute `context_compressor` on type `AIAgent`
plugins/platforms/dingtalk/adapter.py:287: [unresolved-attribute] unresolved-attribute: Attribute `Config` is not defined on `None` in union `Unknown | None`
tests/tools/test_send_message_tool.py:945: [invalid-assignment] invalid-assignment: Object of type `((...) -> Awaitable[dict[Unknown, Unknown]]) | None` is not assignable to attribute `standalone_sender_fn` on type `PlatformEntry | None`
tests/cli/test_cli_provider_resolution.py:378: [unsupported-operator] unsupported-operator: Operator `not in` is not supported between objects of type `Literal["api_mode"]` and `Unknown | None`
tests/hermes_cli/test_managed_scope_env.py:4: [unresolved-import] unresolved-import: Cannot resolve imported module `pytest`
plugins/platforms/dingtalk/adapter.py:1068: [unresolved-attribute] unresolved-attribute: Attribute `CreateCardRequestImRobotOpenSpaceModel` is not defined on `None` in union `Unknown | None`
plugins/platforms/matrix/adapter.py:1872: [invalid-method-override] invalid-method-override: Invalid override of method `send_voice`: Definition is incompatible with `BasePlatformAdapter.send_voice`
plugins/platforms/feishu/feishu_comment.py:173: [unresolved-import] unresolved-import: Cannot resolve imported module `lark_oapi`
plugins/platforms/feishu/feishu_comment.py:40: [unresolved-import] unresolved-import: Cannot resolve imported module `lark_oapi.core.enum`
plugins/platforms/feishu/adapter.py:1370: [unresolved-import] unresolved-import: Cannot resolve imported module `lark_oapi.core.model`
plugins/platforms/feishu/feishu_comment.py:41: [unresolved-import] unresolved-import: Cannot resolve imported module `lark_oapi.core.model.base_request`
plugins/platforms/matrix/adapter.py:1858: [invalid-method-override] invalid-method-override: Invalid override of method `send_document`: Definition is incompatible with `BasePlatformAdapter.send_document`
plugins/platforms/telegram/adapter.py:144: [unresolved-import] unresolved-import: Cannot resolve imported module `telegram.constants`
agent/anthropic_adapter.py:2590: [unresolved-attribute] unresolved-attribute: Attribute `create` is not defined on `None` in union `Any | None`
plugins/platforms/feishu/adapter.py:1357: [unresolved-import] unresolved-import: Cannot resolve imported module `lark_oapi`
plugins/platforms/telegram/adapter.py:4278: [unresolved-attribute] unresolved-attribute: Attribute `MARKDOWN_V2` is not defined on `None` in union `Unknown | None`
plugins/platforms/telegram/adapter.py:135: [unresolved-import] unresolved-import: Module `telegram` has no member `LinkPreviewOptions`
plugins/platforms/feishu/adapter.py:5073: [unresolved-attribute] unresolved-attribute: Attribute `Client` is not defined on `None` in union `Unknown | None`
tests/tools/test_clarify_tool.py:213: [invalid-argument-type] invalid-argument-type: Argument to function `clarify_tool` is incorrect: Expected `list[str] | None`, found `list[str | dict[str, str]]`
plugins/platforms/feishu/adapter.py:3969: [unresolved-import] unresolved-import: Cannot resolve imported module `lark_oapi.api.contact.v3`
... and 345 more

✅ Fixed issues (234):

Rule Count
unresolved-attribute 96
unresolved-import 62
invalid-argument-type 23
invalid-method-override 18
unsupported-operator 11
invalid-assignment 11
call-non-callable 3
invalid-return-type 3
unresolved-reference 2
unresolved-global 1
no-matching-overload 1
not-subscriptable 1
invalid-raise 1
unused-type-ignore-comment 1
First entries
gateway/platforms/telegram.py:4982: [unresolved-attribute] unresolved-attribute: Attribute `GROUP` is not defined on `None` in union `Unknown | None`
gateway/platforms/dingtalk.py:1446: [unresolved-attribute] unresolved-attribute: Attribute `from_dict` is not defined on `None` in union `Unknown | None`
gateway/platforms/feishu.py:1357: [unresolved-import] unresolved-import: Cannot resolve imported module `lark_oapi`
tests/cli/test_reasoning_command.py:552: [invalid-argument-type] invalid-argument-type: Argument to bound method `TestCase.assertIn` is incorrect: Expected `Iterable[Any] | Container[Any]`, found `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 35 union elements`
gateway/platforms/whatsapp.py:875: [invalid-method-override] invalid-method-override: Invalid override of method `send_video`: Definition is incompatible with `BasePlatformAdapter.send_video`
tests/agent/test_curator.py:1105: [unsupported-operator] unsupported-operator: Operator `in` is not supported between objects of type `Literal["curator"]` and `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 35 union elements`
tests/tools/test_web_providers.py:217: [unsupported-operator] unsupported-operator: Operator `in` is not supported between objects of type `Literal["backend"]` and `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 35 union elements`
gateway/platforms/matrix.py:2868: [invalid-argument-type] invalid-argument-type: Argument is incorrect: Expected `list[str]`, found `list[str] | None`
gateway/platforms/feishu.py:3969: [unresolved-import] unresolved-import: Cannot resolve imported module `lark_oapi.api.contact.v3`
gateway/platforms/dingtalk.py:1071: [unresolved-attribute] unresolved-attribute: Attribute `create_card_with_options_async` is not defined on `None` in union `Any | None`
tests/cron/test_suggestions.py:197: [unsupported-operator] unsupported-operator: Operator `in` is not supported between objects of type `Literal["monitor"]` and `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 35 union elements`
gateway/platforms/dingtalk.py:1258: [unresolved-attribute] unresolved-attribute: Attribute `RobotRecallEmotionRequest` is not defined on `None` in union `Unknown | None`
gateway/platforms/email.py:281: [invalid-argument-type] invalid-argument-type: Argument to function `cache_image_from_bytes` is incorrect: Expected `bytes`, found `(Message[str, str] & ~AlwaysFalsy) | (bytes & ~AlwaysFalsy) | (Any & ~AlwaysFalsy)`
hermes_cli/cli_agent_setup_mixin.py:395: [unresolved-global] unresolved-global: Invalid global declaration of `_active_agent_ref`: `_active_agent_ref` has no declarations or bindings in the global scope
gateway/platforms/feishu.py:3397: [unresolved-attribute] unresolved-attribute: Attribute `Response` is not defined on `None` in union `Unknown | None`
gateway/platforms/wecom.py:1480: [invalid-method-override] invalid-method-override: Invalid override of method `send_video`: Definition is incompatible with `BasePlatformAdapter.send_video`
gateway/platforms/matrix.py:3279: [unresolved-attribute] unresolved-attribute: Unresolved attribute `_last_chunk_len` on type `MessageEvent`
gateway/platforms/feishu.py:3537: [unresolved-attribute] unresolved-attribute: Unresolved attribute `_last_chunk_len` on type `MessageEvent`
gateway/platforms/matrix.py:3639: [no-matching-overload] no-matching-overload: No overload of function `min` matches arguments
gateway/platforms/dingtalk.py:888: [invalid-assignment] invalid-assignment: Invalid subscript assignment with key of type `str | None` and value of type `str` on object of type `dict[str, str]`
gateway/platforms/slack.py:1782: [invalid-method-override] invalid-method-override: Invalid override of method `send_image_file`: Definition is incompatible with `BasePlatformAdapter.send_image_file`
gateway/platforms/whatsapp.py:1026: [unresolved-attribute] unresolved-attribute: Unresolved attribute `_last_chunk_len` on type `MessageEvent`
gateway/platforms/telegram.py:1593: [unresolved-attribute] unresolved-attribute: Attribute `updater` is not defined on `None` in union `Unknown | None`
gateway/platforms/matrix.py:1809: [invalid-method-override] invalid-method-override: Invalid override of method `send_image_file`: Definition is incompatible with `BasePlatformAdapter.send_image_file`
gateway/platforms/whatsapp.py:886: [invalid-method-override] invalid-method-override: Invalid override of method `send_voice`: Definition is incompatible with `BasePlatformAdapter.send_voice`
... and 209 more

Unchanged: 5712 pre-existing issues carried over.

Diagnostics are surfaced as warnings — this check never fails the build.

The 301-commit upstream merge introduced one contributor email not yet in
AUTHOR_MAP — fixes the Contributor Attribution Check (check-attribution).
1. conversation_loop.py non-retryable fail-fast (#366) returned
   str(api_error) — a 403 Cloudflare body is ~60 KB of raw HTML that
   leaked into result['error'] and got delivered to chat. Our fail-fast
   path predates upstream's 'summarize non-retryable error' fix; return
   the already-computed _nr_summary instead (matches the other
   non-retryable paths). Fixes test_nonretryable_error_html_summary.

2. test_setup_blank_slate expected the upstream baseline tool set; our
   fork's repo_map (#320) lives in the file toolset so blank-slate
   includes it. Added repo_map to the expected set with a note.
@Lexus2016 Lexus2016 merged commit a3bdf1b into main Jun 21, 2026
39 checks passed
@Lexus2016 Lexus2016 deleted the sync/upstream-2026-06-21 branch June 21, 2026 10:19
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.

[UPSTREAM] Large backlog requires owner review — 258 commits, 11 conflicts