sync: merge upstream/main — 301 commits (2026-06-21), 23 conflicts resolved#405
Merged
Conversation
… 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.
…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).
Contributor
🔎 Lint report:
|
| 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.
This was referenced Jun 21, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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/desktopupdates (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
cron/jobs.pymark_job_started/recover_interrupted_jobs(#105); migrated them onto upstream's new_jobs_lock()(multi-process file lock).cron/scheduler.pyrun_one_job; grafted ourmark_job_started(#105) +strip_reasoning_for_deliveryinto it. Cron-script env now passes our profilerun_envthrough_sanitize_subprocess_env(upstream SECURITY.md §2.3) — verified our no_agent scripts don't need provider env.agent/chat_completion_helpers.pytry_activate_fallback(…, api_error=…); added upstream'srewrite_prompt_model_identity(body already calls it); tookenv_float.run_agent.pycreate_anthropic_message(subsumes our sanitize+create),_session_source_for_agent,env_float, background policy (_is_subagent) — kept ourhandoff_mode.tools/delegate_tool.py_model_background_value— kept ourhandoff_mode.tools/mcp_tool.py__slots__superset (+_elicitation/_pending_call_context/_ping_unsupported) and_get_secret(env-fallback).tools/cronjob_tools.py_reset_cron_failure(ours) +_notify_provider_jobs_changed_safe(upstream) at all 5 sites.agent/turn_retry_state.py(+test)fail_fast_attempted(ours) +auth_failover_attempted(upstream).nix/lib.niximportNpmLock(removes our npmDepsHash sync churn).scripts/release.pyAUTHOR_MAP.README.md: kept fork identity.Testing
compileallacrossagent/cron/tools/scripts/hermes_cliclean.tests/cron493+ pass. Ourtest_script_reads_env_from_hermes_dotenv(adapted to the new env policy) + upstream'stest_script_subprocess_env_sanitizedboth pass. Our suites (prompt_builder, self_critique, security_guidance, turn_retry_state) green (212 passed).croniter/prompt_toolkit/signal-delivery → sometests/cron/tests/run_agentcases 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.