Skip to content

Merge upstream NousResearch/main into fork (2026-06-29 parity sync) - #119

Merged
Kyzcreig merged 1735 commits into
mainfrom
sync/upstream-2026-06-29
Jun 30, 2026
Merged

Kyzcreig merged 1735 commits into
mainfrom
sync/upstream-2026-06-29

Conversation

@Kyzcreig

Copy link
Copy Markdown
Collaborator

Upstream parity sync — NousResearch/main → fork main (2026-06-29)

Brings the Kyzcreig fork to parity with frozen upstream target 929dd9c0d (2026-06-29 12:09 CT). True merge commit, 2 parents, HEAD..929dd9c0d = 0.

Divergence at exec: fork was 222 ahead / 1734 behind (1546 non-merge upstream commits). Config version 29 → 32 (3 migrations — dry-run gated before any deploy, not in this PR).

Conflict resolution (36 files / 84 hunks, 0 leftover markers)

  • mem0: took fork's self-contained 1653-line __init__.py wholesale (hybrid retrieval, _DirectRestMem0Client, dedup ladder); git rm'd upstream's orphaned _backend.py/_oss_providers.py/_setup.py + tests. 371 mem0 tests pass. Rationale: docs/sync/review/mem0-resolution-decision.md.
  • telegram: architectural port to new plugins/platforms/telegram/adapter.py — kept fork's network-retry ladder + upstream's heartbeat-stuck probe.
  • gateway/run.py, context_compressor, auxiliary_client, background_review, cron/scheduler, kanban_db, delegate_tool, hermes_state, conversation_loop, usage_pricing, toolsets, hermes_cli/{backup,commands,config,models} + 13 test files — keep-both / union / fork-primary per the per-hunk log.

Real merge regressions caught by gates and fixed (not test-fudging)

Hygiene-limit flip (400→5000) restored · in-place compaction announce re-threaded · _kanban_worker_skill_available restored · redact git-identity allowlist · provider_label UnboundLocalError · include_timestamp gating · trigram FTS rebuild · agent_init.py _wrapped NameError (LCM-critical) · over-folded cron failure framing · stale monolith imports. Plus the two final deltas this session:

  1. test_ignore_user_config_flags — stray untracked cli-config.yaml artifact (not a merge issue), removed.
  2. test_run_agent_reasoning — fork test asserted pre-Cerebras API rejects reasoning_content on multi-turn replay (HTTP 400) NousResearch/hermes-agent#45655 preserve-always reasoning_content; updated to an echo-back (Kimi) provider so it validates the merged Cerebras API rejects reasoning_content on multi-turn replay (HTTP 400) NousResearch/hermes-agent#45655 semantics (strip for non-echo-back providers).

Full hermetic suite (~35,093 tests, per-file isolation)

0 merge regressions. Every failing/timeout file is byte-identical to upstream and fails for an env-specific reason (macOS lacks systemd · model-catalog HTTP 403 · restricted PATH omits /usr/sbin lsof · /tmp→/private/tmp symlink · macOS zombie-reaping · HOME=$HOME cred pollution · anaconda rg 0.10.0 · bridge inspect.getsource wrapper · per-file isolation / slow timeouts). Full classification: docs/sync/review/fullsuite-final-delta-classification.md. Expected green on the Linux fleet + CI.

CI-gate prep (validated locally)

  • contributor-check: added 4 upstream-author mappings to scripts/release.py AUTHOR_MAP (lEWFkRAD, chrispersico, KeyArgo, wnuuee1) — 0 unmapped.
  • fleet-secret-scan (gitleaks): extended .gitleaks.toml to allowlist 24 new upstream test-fixture/doc-placeholder/public-OAuth-client-ID files. Triaged each — no real secret. Negative test confirms a real high-entropy secret in those source files is still caught (surgical regex, not blanket).
  • uv-lockfile-check ✅ · history-check ✅ · semgrep ERROR gate ✅.

Docs

docs/sync/ — PRD (v0.3, 2-pass Opus review folded), PLAN, cron-sync spec, per-pass review logs, delta audits, resolution-decision records.


⚠️ Do not auto-merge. Awaiting Ace's review (esp. mem0 + LCM/compaction reconciliation). Pre-deploy gate = config-migration dry-run 29→32 against real fleet snapshots; deploy = hermes update per host (host-1 ≠ this gateway), staged.

logicsunny and others added 30 commits June 28, 2026 01:15
…terns

The MiniMax output-layer safety filter surfaces the error verbatim as
`output new_sensitive (1027)` (sometimes with additional provider
wrapping like 'Stream stalled mid tool-call: output new_sensitive (1027)').
When the model emits a large tool-call argument block, the upstream
filter trips and the SSE stream is truncated mid-flight, producing
'stream stalled mid tool-call' errors. Until now this case was
misclassified and retried 3x on the same provider, reproducing the same
refusal and burning paid attempts.

Adding `new_sensitive` to `_CONTENT_POLICY_BLOCKED_PATTERNS` routes
it through the existing is_client_error path: skip 3x retry, activate
configured fallback model immediately, surface a clear provider-safety
message to the user.

Refs NousResearch#32421
…sResearch#32421)

When a provider's output-layer safety filter (MiniMax "output new_sensitive
(1027)", Azure content_filter, etc.) kills a streaming response after deltas
were already sent, interruptible_streaming_api_call swallows the raw error
into a finish_reason=length partial-stream stub. The conversation loop then
burned 3 continuation retries against the SAME primary — re-hitting the
content-deterministic filter every time — and gave up with "Response remained
truncated after 3 continuation attempts", never consulting fallback_providers.

Builds on @595650661's classifier change (cherry-picked) so error_classifier
recognizes the filter; then:
- chat_completion_helpers: run the swallowed error through error_classifier at
  the stub-creation point and stamp _content_filter_terminated on the stub
  (single source of truth — no parallel pattern list).
- conversation_loop: read the tag and activate the fallback chain BEFORE
  burning any continuation retries; roll partial content back to the last
  clean turn and re-issue against the new provider (restart_with_rebuilt_messages).
  Plain network stalls are unaffected (only content_policy_blocked is tagged).

Credits NousResearch#32479 (@sweetcornna) and NousResearch#33845 (@Tranquil-Flow) which fixed the
same issue via the stub-tag and loop-escalation approaches respectively.

Live E2E confirmed: before, _try_activate_fallback called 0x; after, fallback
fires on the first stub and the fallback provider completes the turn.
…#32791) (NousResearch#54063)

* docs(discord): document bot-to-bot comms as unsupported (NousResearch#32791)

Multi-profile bot-to-bot conversation is not a supported topology.
DISCORD_ALLOW_BOTS=none (the default) blocks all bot-originated
messages; setting mentions/all across multiple Hermes profiles to make
them reply to each other ack-loops because Discord's reply auto-mention
satisfies the mention gate every turn. Document the safe default and
the loop hazard so operators don't wire it up.

* docs(discord): infographic for bot-to-bot unsupported stance (NousResearch#32791)
…ousResearch#33801) (NousResearch#54061)

Secret redaction is display/output-scoped on main — write_file writes
content verbatim, terminal/execute_code redact only output not the
command/source. The real bug is in displayed tool OUTPUT (read_file,
terminal, execute_code):

_DB_CONNSTR_RE's password group [^@]+ was greedy across newlines, so on a
multi-line block it scanned past the DSN line to the next stray '@' (a
Python @decorator), replacing every intervening character — including line
breaks — with ***. That dropped lines and concatenated the next line onto
the f-string line, making read_file output look corrupted (the file on disk
was always correct). Reported in NousResearch#33801.

Fix:
- Forbid whitespace in the userinfo/password groups ([^:\s]+ / [^@\s]+) so
  the match can never span a line break. A real DSN password never contains
  whitespace. This alone kills the catastrophic line-dropping.
- Under code_file=True, preserve a password group that is a pure {...} brace
  expression — f"postgresql://{user}:{pass}@{host}" is an f-string template,
  not a live credential. Literal passwords are still masked.
- Pass code_file=True at the terminal and execute_code output redaction call
  sites (file_tools already did) so code-execution output isn't corrupted by
  ENV/JSON/template false positives. Real prefixes, auth headers, JWTs, and
  private keys are still redacted.

Verified E2E against the reporter's exact pydantic-settings module: file
written verbatim, read_file shows the DSN f-string + @model_validator intact
with zero *** corruption, while a literal postgresql://admin:pw@host DSN and
a real sk- key are still masked.

Reported-by: koishi70
Reported-by: pfrenssen
…Research#35205) (NousResearch#54076)

* fix(telegram): clear send_path_degraded on successful reconnect

_send_path_degraded was cleared only in _verify_polling_after_reconnect,
60s after reconnect and only if scheduled. A clean start_polling() reconnect
left the flag stuck True, short-circuiting send() and blocking all outbound
messages until the deferred probe ran (or forever if it never did).

Clear the flag the moment start_polling() succeeds — that is the recovery
signal. The deferred probe remains a defensive re-check that re-enters the
reconnect ladder (re-setting the flag) if it detects a silent wedge.

Fixes NousResearch#35205.

* docs: add infographic for NousResearch#35205 telegram send-path fix
…ousResearch#36658)

Dashboard /chat spawns the TUI attached to the dashboard's in-memory
gateway via HERMES_TUI_GATEWAY_URL. In that attach mode the already-running
gateway replays `gateway.ready` (and `session.info`) the instant the socket
connects, so those events land in GatewayClient.bufferedEvents *before* the
consumer's mount-time subscribe effect (useMainApp.ts) calls drain().

drain() then emitted the buffered events synchronously, so the
`gateway.ready` handler's patchUiState / setHistoryItems cascade ran while
React was still inside the first commit — tripping "Too many re-renders"
(Minified React error #301) and breaking Dashboard chat after `hermes update`.
Spawn / inline / sidecar modes never hit this: their `gateway.ready` only
arrives after the Python child boots, on a later async tick.

Fix: drain() defers the replay to the next microtask AND keeps `subscribed`
false until that microtask runs. Keeping `subscribed` false in the gap means
any live event arriving before the flush keeps buffering (publish() pushes
when !subscribed) instead of emitting synchronously and jumping ahead of the
chronologically-earlier replayed events — the flush re-drains the buffer
right after flipping `subscribed`, preserving FIFO order. A drainGeneration
token (bumped in resetStartupState) makes a queued flush a no-op if the
transport was reset/killed in the meantime, avoiding use-after-teardown and
duplicate/reordered exits.

Regression tests: (1) drain() does not dispatch buffered events synchronously;
(2) a live event arriving in the post-drain / pre-microtask window still
delivers BEHIND the earlier-buffered event (FIFO). Both are red against the
old synchronous behavior, green with this fix. Same class of fix as NousResearch#44528.

Closes NousResearch#36658
…usResearch#34500)

A custom_providers config that names the model under model.name (or
model.model) resolved to an empty model, so the API request went out
with model= — HTTP 400 from OpenAI-compatible backends. Display paths
(hermes status/dump) already read model.name and showed the model,
making the failure silent.

The model id was read via 'default or model' at ~14 independent sites
(cli, gateway, cron, curator, oneshot, fallback, profiles, ...), none
of which honored 'name'. Rather than patch every site, canonicalize at
the single load/save chokepoint: _normalize_root_model_keys() now
promotes model.model/model.name -> model.default (precedence
default > model > name) and drops the stale alias, so every reader —
present and future — sees a populated default and config.yaml is
migrated canonical on next save. The gateway, which bypasses
load_config(), replays the same normalization in _load_gateway_config().

Co-authored-by: Bartok9 <danielrpike9@gmail.com>

Credit: root-cause analysis and fix direction from @Bartok9 (NousResearch#34502,
first) and @v86861062 (NousResearch#34527).
…cognized host (NousResearch#35166)

On apt releases newer than the bundled Playwright recognizes (Ubuntu 26.04,
Debian 14, and future distros), 'npx playwright install --with-deps chromium'
hangs uninterruptibly at 'Installing Playwright Chromium with system
dependencies' because Playwright's resolver maps the host to a platform with
no download build (NousResearch#35166).

Wrap every installer Playwright call in run_playwright_install(), which tries
the native install first and, only if it fails or times out, retries once with
PLAYWRIGHT_HOST_PLATFORM_OVERRIDE pinned to the newest known build
(ubuntu24.04-<arch>). This is the escape hatch Playwright's maintainers bless
for unrecognized platforms (microsoft/playwright#33434).

Try-native-first (not a hardcoded distro/version table) is deliberate:
- Self-correcting — when Playwright already supports the host (e.g. Ubuntu
  26.04 on Playwright >=1.61) the first attempt succeeds and the override is
  never applied, so we never force a mismatched-glibc build onto a release
  Playwright handles correctly (microsoft/playwright#35114).
- Zero-maintenance — new distro releases work the moment Playwright adds them.
- Covers Debian 14+ and future releases, not just Ubuntu 26.04.

An operator-set PLAYWRIGHT_HOST_PLATFORM_OVERRIDE is always respected (applied
to the first attempt; retry skipped). Non-x64/arm64 arches have no fallback
build and skip the retry.

Refs NousResearch#35166
…p step interruptible

Follow-up on NousResearch#54032 for NousResearch#35166:
- Gate the PLAYWRIGHT_HOST_PLATFORM_OVERRIDE retry on the host being an apt
  release newer than Playwright recognizes (Ubuntu >24.04 / Debian >13) via
  playwright_host_unrecognized(), instead of retrying on ANY install failure.
  A network/disk/permission failure on a supported host now surfaces unchanged
  rather than getting a mismatched-glibc build forced onto it.
- detect_os() now captures DISTRO_VERSION from os-release.
- Fold in the interruptibility fix (was PR NousResearch#35304, self-closed): wrap the
  download in 'timeout --foreground -k 10' (probed, with plain-timeout
  fallback) so a terminal Ctrl+C reaches the child and a wedged download is
  force-killed after the deadline.
- Add behavioral tests that source the helpers and assert the retry fires only
  on Ubuntu 26.04 / Debian 14, not on supported hosts, non-apt distros,
  native-success, operator-pinned override, or unsupported arch.
When an LLM API call returns HTTP 4xx with an empty parsed SDK `body` ({}),
`_summarize_api_error` fell through to a bare `str(error)`, so users saw only
"HTTP 400" with no provider detail (reported on Windows in NousResearch#36109). The SDK
leaves `body` empty in this case, but the httpx `response` still carries the
payload in `.text`.

- run_agent.py `_summarize_api_error`: when `body` is empty, fall back to
  `response.text` — parse a JSON `error.message`/`message` when present, else
  surface the raw (truncated) body. Platform-agnostic diagnostics.
- hermes_cli/oneshot.py: `hermes -z` now runs via `run_conversation` and returns
  exit code 2 when the run is failed/partial with no usable final response, so
  scripts can detect LLM failures (still 0 when a response — incl. an error
  summary as output — is produced).

Tests: new tests/run_agent/test_summarize_api_error.py (empty-body JSON + raw
text, RED/GREEN verified) + oneshot exit-code/`run_conversation` wiring tests.

NOTE: NousResearch#36109's original root cause (Windows "all providers return empty 400")
is not reproducible on current main (heavy provider-transport churn since
v0.15.1). This change does not claim to fix that root cause — it makes any
empty-body API error LEGIBLE so a future occurrence shows the real provider
message instead of a bare HTTP 400. Relates to NousResearch#36109 (does not close it).
expand_whatsapp_aliases hardcoded get_hermes_home()/whatsapp/session, but
the adapter writes lid-mapping files via get_hermes_dir("platforms/whatsapp/
session", "whatsapp/session"). On installs without the legacy directory the
two paths diverge, so the resolver finds no mappings and returns the bare LID,
which misses the allowlist and silently drops the message. Resolve through the
same helper so both sides stay in lockstep on new and legacy layouts.
Add an _is_user_authorized E2E for the platforms/whatsapp/session layout
on top of fesalfayed's resolver fix (NousResearch#36665) — guards the actual
silently-dropped-LID-sender path from NousResearch#36664.
…chats (NousResearch#39293)

Extend the gateway noisy-status filter beyond Telegram so internal
compression lifecycle messages stay in logs instead of spamming Discord,
Slack, and other messaging channels.
Force API-server error text through the existing secret redactor before returning OpenAI-compatible errors, response fallback text, response snapshots, and run failure events. This prevents credential-shaped provider failure text from crossing the API-server boundary while preserving debuggable sanitized messages.
Follow-up to the salvaged NousResearch#37733 fix. The contributor centralized
redaction at _openai_error and the chat/responses failure paths, which
covers the OpenAI-compatible envelopes transitively. Two sibling classes
crossed the same authenticated HTTP boundary unredacted:

- 8x cron-management endpoints returning {"error": str(e)} on 500
- the session-chat SSE error event ({"message": str(exc)})

Route both through the same _redact_api_error_text(force=True) helper.
Add AUTHOR_MAP entry for coygeek and a TestRedactApiErrorText guard
covering mask/force/limit/passthrough behavior.
When tools.environments.local can't be imported (partial install,
import-time error), _is_hermes_provider_credential() returned False —
fail-open. A skill could then register a Hermes provider credential
(ANTHROPIC_API_KEY, etc.) as env passthrough; _scrub_child_env lets
passthrough vars bypass the secret-substring net (rule 1), so the
operator's real key would land in the execute_code child. Reopens the
GHSA-rhgp-j443-p4rf bypass.

Fail closed instead: on import failure, treat the name as a protected
provider credential and refuse passthrough. Regression test exercises
the full register -> scrub path under a simulated import failure.

Co-authored-by: Hermes Agent <noreply@nousresearch.com>
Long-lived helpers spawned indirectly by tool calls (adb, platform
bridges) were left in the service cgroup after the gateway's main
process exited. When the kernel rejected the deferred cgroup-wide kill
with EINVAL, systemd blocked Restart=always for 6+ minutes, taking
down all platforms and cron windows (NousResearch#37454).

Add a small ExecStopPost helper (gateway.cgroup_cleanup) that walks
cgroup.procs and sends per-PID SIGKILLs — a different kernel code path
than cgroup.kill, so it succeeds where the cgroup-wide write failed.
KillMode=mixed is preserved so the gateway still reaps its own
tool-call children before systemd intervenes (NousResearch#8202).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- read_text(encoding='utf-8') (PLW1514)
- # windows-footgun: ok on signal.SIGKILL — module is Linux-only (reads
  /proc, /sys/fs/cgroup; runs from a systemd unit)
- test lambda accepts the new encoding kwarg
Fix race condition in terminal environment snapshots that could corrupt
PATH with declare -x entries. When concurrent terminal calls share the
same snapshot file, the non-atomic 'export -p > snapshot.sh' write could
be read mid-write by another process, causing partial/corrupted env vars
to be sourced and mixed into PATH.

The fix uses atomic file replacement:
- Write to a temp file: export -p > snapshot.sh.tmp.303651
- Atomically replace: mv -f snapshot.sh.tmp.303651 snapshot.sh

On POSIX, mv within the same filesystem is atomic, so source() will
either see the old complete snapshot or the new complete one, never a
partial/truncated file.

Fixes NousResearch#38249
…lure path

The atomic mv approach (kyssta-exe's commit) narrows but does not close the
NousResearch#38249 race: the temp name used $$ (parent shell PID), which is identical
across &-launched concurrent subshells. Two concurrent writers pick the same
temp file, clobber each other mid-write, and mv then publishes a torn snapshot
— a reader sourcing it absorbs declare-x/export fragments into PATH.

- Use $BASHPID (actual per-subshell PID) so concurrent writers never collide.
- Chain mv on export success (&&) and rm the temp on failure so a partial dump
  never replaces a good snapshot; apply the same to the init_session bootstrap.
- shlex-quote the static temp-path portion (Windows/spaces), $BASHPID outside.
- LocalEnvironment.cleanup sweeps orphaned snap.tmp.* temps.
- Regression tests: string-shape + a behavioral concurrent writers/readers test
  that proves the snapshot never tears (would still tear with $$).
…teway-drain-microtask

fix(tui): defer buffered gateway events to stop dashboard chat #301 (NousResearch#36658)
Co-Authored-By: Paperclip <noreply@paperclip.ing>
teknium1 and others added 14 commits June 29, 2026 04:25
The reset-had-activity tests set total_tokens (dead state) to simulate
activity; production records activity via last_prompt_tokens. Update
the fixtures to match the field the fix and runtime actually use.
…ersal

Session IDs can originate from untrusted input (e.g. the
X-Hermes-Session-Id API header) and are interpolated raw into on-disk
artifact filenames under ~/.hermes/sessions/. A traversal-shaped ID
(../../../../etc/pwned) would let a caller write the session snapshot
or request dump outside the sessions directory.

_safe_session_filename_component() collapses every non [A-Za-z0-9_-]
character to _, caps the length, and appends a short content hash when
sanitization changed the string, always yielding a single traversal-free
path segment.

Closes NousResearch#5958.
…undary

Defense-in-depth on top of _safe_session_filename_component (NousResearch#5958):

Sink (makes the bad write impossible regardless of entry point):
- run_agent._save_session_log: sanitize session_id before building the
  session_{sid}.json snapshot path.
- agent_runtime_helpers.dump_api_request_debug: sanitize before building
  the request_dump_{sid}_{ts}.json path.

Boundary (clean 400 instead of a silently-hashed filename):
- api_server rejects path-traversal-shaped X-Hermes-Session-Id on the
  session-continuation path and the explicit /api/sessions create path,
  reusing gateway.session._is_path_unsafe (mirrors the native gateway's
  entry-boundary guard). Also enforces the session-header length cap on
  the continuation path.

Tests: traversal session_id stays contained at the write site; sanitizer
always yields a traversal-free segment; the API header rejects
../, absolute, and Windows-traversal IDs with 400.
Widen NousResearch#5961's _format_untrusted_prompt_value coverage to the Matrix
room display name (**Matrix Room:**), a sibling attacker-controllable
field the original fix missed. chat_name is user-settable, so an
injected room name could render as literal markdown in the system
prompt. Adds a regression test.
NousResearch#54834)

The register path builds each profile-gateway slot in a sibling staging
dir under /run/service (the scandir s6-svscan watches), then atomically
renames it to the live gateway-<profile> name. The staging dir was named
gateway-<profile>.tmp — a NON-dotfile — so a concurrent `s6-svscanctl -a`
rescan (fired by the cont-init reconciler registering gateway-default, or
by a sibling register) would supervise the half-built slot the moment it
had a valid type/run: s6-supervise spawns AS ROOT and mkdirs supervise/
root-owned 0700, then the in-flight _seed_supervise_skeleton early-returns
on the now-existing supervise/ and the next `mkdir supervise/event` hits
PermissionError.

That is the arm64-only CI flake on
test_s6_unregister_removes_service_dir_in_live_container
(PermissionError: /run/service/gateway-phase3test.tmp/supervise/event) —
arm64-only because the native-arm runner's wider scheduling jitter lets
the rescan land inside the ~ms seed window; amd64 ran 30/30 clean.

Fix: dot-prefix the staging dir (.gateway-<profile>.tmp) in both register
paths (S6ServiceManager.register_profile_gateway and
container_boot._register_service). s6-svscan skips any scandir entry whose
name begins with '.', so the half-built slot can never be supervised
mid-build. The atomic rename to the dotless live name is unchanged.

Verified on a real s6 image (amd64): a non-dotted staging dir is picked up
by an svscanctl -a rescan (SUPERVISED owner=root) while a dot-prefixed one
is ignored (NOT-SUPERVISED). Added a docker-harness regression test that
asserts both, plus a unit test that the staging dir is dot-prefixed.
NVIDIA integrate.api.nvidia.com models such as minimaxai/minimax-m3 can
return HTTP 200 with empty choices when max_tokens is omitted. Keep the
output cap on auxiliary chat-completions routes, matching the main NVIDIA
provider profile behavior.
Let users click the status bar context indicator to see how tokens are
split across system prompt, tools, rules, skills, MCP, and conversation.

Co-authored-by: Cursor <cursoragent@cursor.com>
…ontext-usage-popover

feat(desktop): add context usage breakdown popover
…sResearch#7779) (NousResearch#54862)

A manually-installed venv inside the cloned repo can be destroyed by the
agent running a relative-path command against its own checkout (rm -rf venv,
uv venv venv, etc.), silently wiping the running runtime mid-session. Moving
the canonical manual-install venv to ~/.hermes/venvs/hermes-dev means no
relative path from the agent's workspace resolves to its own runtime, making
the bug class impossible without any command-detection code.

Closes the root cause of NousResearch#7779. The managed install.sh layout is unchanged.
…ousResearch#54843)

* feat(web_extract): truncate-and-store instead of LLM summarization

web_extract no longer runs an auxiliary LLM over scraped pages. The extract
backends (Firecrawl/Tavily/Exa/Parallel) already return clean, boilerplate-
stripped markdown, so we return it directly: pages within a char budget
(default 15000, web.extract_char_limit) come back whole; larger pages get a
head+tail window plus an explicit footer giving the stored full-text path and
the read_file call to page through the omitted middle. The full clean text is
written to cache/web (mounted read-only into remote backends like the other
cache dirs), so nothing is lost.

Inline base64 images are converted to [IMAGE: alt] placeholders (token bombs
dropped) while real http(s) image URLs are preserved as links so the agent can
still web_extract/vision_analyze them.

Removes process_content_with_llm + the chunked summarizer + check_auxiliary_model
+ _resolve_web_extract_auxiliary. context_references._default_url_fetcher is
updated to the truncate path and its stale data.documents shape read is fixed
to results (it was silently returning empty).

Live before/after eval (firecrawl, 4 URLs): 11.7x faster overall (176.6s ->
15.1s); 10-60x on large pages. Quality identical; findability 4/4 (answer
recoverable from stored full text on every truncated page). web_search is
unchanged.

No own scraper added; no changes to web_search.

* fix(web_extract): add char_limit to execute_code web_extract stub

The new web_extract char_limit param must appear in the code_execution_tool
_TOOL_STUBS signature (and doc line) or test_stubs_cover_all_schema_params
fails — the stub schema must cover every real schema param.
Subagent session pop-outs (`watch=1`) spectate a run driven elsewhere, so
editing/steering the transcript from there makes no sense. Gate the composer
and the user-bubble mutations on `isWatchWindow()`:

- hide the composer (folds into `showChatBar`)
- user prompts become a read-only button that toggles the 2-line clamp so long
  prompts stay fully readable, instead of opening the edit composer
- drop the stop/restore actions and the checkpoint branch-picker

Keyed off the narrow `isWatchWindow()` (not `isSecondaryWindow()`), so the
new-session and cmd-click pop-outs are unaffected.
…atch-readonly

feat(desktop): read-only spectator transcript for subagent watch windows
@greptile-apps

greptile-apps Bot commented Jun 30, 2026 •

Copy link
Copy Markdown

Too many files changed for review. (2080 files found, 100 file limit)

@github-actions

Copy link
Copy Markdown

🚨 CRITICAL Supply Chain Risk Detected

This PR contains a pattern that has been used in real supply chain attacks. A maintainer must review the flagged code carefully before merging.

🚨 CRITICAL: Install-hook file added or modified

These files can execute code during package installation or interpreter startup.

Files:

setup.py

Scanner only fires on high-signal indicators: .pth files, base64+exec/eval combos, subprocess with encoded commands, or install-hook files. Low-signal warnings were removed intentionally — if you're seeing this comment, the finding is worth inspecting.

@github-actions

github-actions Bot commented Jun 30, 2026 •

Copy link
Copy Markdown

🔎 Lint report: sync/upstream-2026-06-29 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: 12205 on HEAD, 11081 on base (🆕 +1124)

🆕 New issues (950):

Rule Count
unresolved-import 312
unresolved-attribute 271
invalid-argument-type 191
invalid-assignment 47
not-subscriptable 38
unsupported-operator 33
invalid-method-override 25
invalid-return-type 10
unresolved-reference 6
call-non-callable 5
no-matching-overload 5
unused-type-ignore-comment 3
not-iterable 3
invalid-parameter-default 1
First entries
tests/tools/test_file_tools_tilde_profile.py:21: [unresolved-import] unresolved-import: Cannot resolve imported module `pytest`
tests/gateway/relay/test_relay_passthrough.py:44: [invalid-argument-type] invalid-argument-type: Argument to `RelayAdapter.__init__` is incorrect: Expected `RelayTransport | None`, found `StubConnector`
tools/computer_use/doctor.py:114: [unresolved-attribute] unresolved-attribute: Attribute `readline` is not defined on `None` in union `IO[Any] | None`
tests/hermes_cli/test_projects_cli.py:7: [unresolved-import] unresolved-import: Cannot resolve imported module `pytest`
tests/honcho_plugin/test_oauth_flow.py:16: [unresolved-import] unresolved-import: Cannot resolve imported module `httpx`
tests/test_desktop_electron_pin.py:31: [unresolved-import] unresolved-import: Cannot resolve imported module `pytest`
plugins/web/xai/provider.py:186: [unresolved-attribute] unresolved-attribute: Attribute `strip` is not defined on `None` in union `Any | None | Literal["grok-build-0.1"]`
tests/cli/test_resume_display.py:717: [not-subscriptable] not-subscriptable: Cannot subscript object of type `float` with no `__getitem__` method
tests/agent/test_non_stream_stale_timeout.py:36: [invalid-argument-type] invalid-argument-type: Argument to `AIAgent.__init__` is incorrect: Expected `((str, dict[Unknown, Unknown], /) -> None) | None`, found `str | bool`
tests/gateway/test_telegram_username_chat_id.py:120: [unresolved-attribute] unresolved-attribute: Unresolved attribute `InlineKeyboardMarkup` on type `ModuleType`
tests/tools/test_computer_use.py:1889: [invalid-parameter-default] invalid-parameter-default: Default value of type `None` is not assignable to annotated parameter type `Exception`
gateway/platforms/qqbot/adapter.py:281: [invalid-method-override] invalid-method-override: Invalid override of method `connect`: Definition is incompatible with `BasePlatformAdapter.connect`
tests/tools/test_send_message_tool.py:1108: [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/cron/test_scheduler_provider.py:192: [not-subscriptable] not-subscriptable: Cannot subscript object of type `None` with no `__getitem__` method
tests/hermes_cli/test_managed_scope_regression.py:9: [unresolved-import] unresolved-import: Cannot resolve imported module `pytest`
tests/plugins/memory/test_memory_lazy_install.py:34: [unresolved-import] unresolved-import: Cannot resolve imported module `pytest`
plugins/platforms/wecom/adapter.py:48: [unresolved-import] unresolved-import: Cannot resolve imported module `aiohttp`
tests/gateway/test_media_metadata_contract.py:19: [unresolved-import] unresolved-import: Cannot resolve imported module `pytest`
tests/gateway/relay/test_relay_interrupt.py:37: [invalid-argument-type] invalid-argument-type: Argument to `RelayAdapter.__init__` is incorrect: Expected `RelayTransport | None`, found `StubConnector`
plugins/platforms/feishu/adapter.py:1358: [unresolved-import] unresolved-import: Cannot resolve imported module `lark_oapi`
tests/gateway/relay/test_descriptor.py:33: [invalid-assignment] invalid-assignment: Property `max_message_length` defined in `CapabilityDescriptor` is read-only
tests/hermes_cli/test_billing_cli.py:13: [unresolved-import] unresolved-import: Cannot resolve imported module `pytest`
tests/run_agent/test_28161_anthropic_stream_pool_cleanup.py:40: [invalid-argument-type] invalid-argument-type: Argument to `AIAgent.__init__` is incorrect: Expected `int | float`, found `str | bool`
tests/hermes_cli/test_setup_noninteractive.py:122: [not-subscriptable] not-subscriptable: Cannot subscript object of type `float` with no `__getitem__` method
tests/gateway/test_telegram_closewait_limits_31599.py:33: [unresolved-import] unresolved-import: Cannot resolve imported module `pytest`
... and 925 more

✅ Fixed issues (336):

Rule Count
unresolved-attribute 117
unresolved-import 77
invalid-argument-type 61
invalid-method-override 20
invalid-assignment 19
unsupported-operator 16
invalid-return-type 8
no-matching-overload 7
call-non-callable 3
unresolved-reference 2
unknown-argument 2
not-subscriptable 1
invalid-parameter-default 1
unused-type-ignore-comment 1
unresolved-global 1
First entries
gateway/run.py:16102: [invalid-argument-type] invalid-argument-type: Argument to bound method `AIAgent.run_conversation` is incorrect: Expected `str | None`, found `Any | str | list[dict[str, Any]]`
gateway/platforms/feishu.py:1368: [unresolved-import] unresolved-import: Cannot resolve imported module `lark_oapi.core`
gateway/platforms/whatsapp.py:347: [invalid-argument-type] invalid-argument-type: Argument to constructor `Path.__new__` is incorrect: Expected `str | PathLike[str]`, found `str | None`
gateway/platforms/feishu.py:3969: [unresolved-import] unresolved-import: Cannot resolve imported module `lark_oapi.api.contact.v3`
tests/tools/test_stage2_hook_user_flag_guard.py:31: [unresolved-import] unresolved-import: Cannot resolve imported module `pytest`
gateway/platforms/slack.py:91: [unresolved-import] unresolved-import: Cannot resolve imported module `slack_bolt.async_app`
gateway/platforms/matrix.py:1247: [unresolved-import] unresolved-import: Cannot resolve imported module `mautrix.crypto.store.asyncpg`
gateway/platforms/sms.py:292: [unresolved-reference] unresolved-reference: Name `aiohttp` used when not defined
gateway/run.py:8509: [invalid-argument-type] invalid-argument-type: Argument to function `get_disabled_skill_names` is incorrect: Expected `str | None`, found `Literal["local", "telegram", "discord", "whatsapp", "whatsapp_cloud", ... omitted 18 literals] | (set[Unknown] & ~AlwaysFalsy)`
tests/skills/test_google_oauth_setup.py:365: [unresolved-import] unresolved-import: Cannot resolve imported module `googleapiclient`
gateway/platforms/telegram.py:4060: [unresolved-attribute] unresolved-attribute: Attribute `HTML` is not defined on `None` in union `Unknown | None`
gateway/platforms/dingtalk.py:921: [unresolved-attribute] unresolved-attribute: Attribute `TimeoutException` is not defined on `None` in union `Unknown | None`
tests/agent/test_gemini_cloudcode.py:334: [unresolved-attribute] unresolved-attribute: Attribute `refresh_token` is not defined on `None` in union `GoogleCredentials | None`
tools/delegate_tool.py:2419: [invalid-argument-type] invalid-argument-type: Argument to function `_run_single_child` is incorrect: Expected `str`, found `Unknown | str | None | list[str]`
gateway/slash_commands.py:1268: [invalid-argument-type] invalid-argument-type: Argument to function `list_authenticated_providers` is incorrect: Expected `dict[Unknown, Unknown]`, found `None | Unknown`
gateway/platforms/feishu_comment.py:1089: [invalid-argument-type] invalid-argument-type: Argument to bound method `AIAgent.run_conversation` is incorrect: Expected `list[dict[str, Any]]`, found `(list[dict[str, Any]] & ~AlwaysFalsy) | None`
gateway/platforms/feishu.py:2993: [unresolved-import] unresolved-import: Cannot resolve imported module `lark_oapi.api.im.v1`
tools/approval.py:1497: [unresolved-attribute] unresolved-attribute: Attribute `get` is not defined on `str` in union `str | Unknown`
gateway/run.py:16102: [invalid-argument-type] invalid-argument-type: Argument to bound method `AIAgent.run_conversation` is incorrect: Expected `str`, found `Any | str | list[dict[str, Any]]`
gateway/platforms/matrix.py:1248: [unresolved-import] unresolved-import: Cannot resolve imported module `mautrix.util.async_db`
gateway/platforms/dingtalk.py:78: [unresolved-import] unresolved-import: Cannot resolve imported module `alibabacloud_tea_util`
gateway/slash_commands.py:1859: [invalid-argument-type] invalid-argument-type: Argument to function `_home_target_env_var` is incorrect: Expected `str`, found `Literal["local", "telegram", "discord", "whatsapp", "whatsapp_cloud", ... omitted 18 literals] | set[Unknown]`
tests/hermes_cli/test_aux_config.py:54: [unresolved-attribute] unresolved-attribute: Attribute `keys` is not defined on `str`, `list[Unknown]`, `list[str]`, `None`, `int` in union `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 32 union elements`
gateway/platforms/feishu.py:1286: [unresolved-import] unresolved-import: Cannot resolve imported module `lark_oapi.ws.client`
tools/delegate_tool.py:2433: [invalid-argument-type] invalid-argument-type: Argument to function `dispatch_async_delegation` is incorrect: Expected `list[str] | None`, found `(Any & ~AlwaysFalsy) | (str & ~AlwaysFalsy) | list[str] | None`
... and 311 more

Unchanged: 5443 pre-existing issues carried over.

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

@Kyzcreig
Kyzcreig force-pushed the sync/upstream-2026-06-29 branch from 2449339 to 23d6c3c Compare June 30, 2026 02:51
@github-actions

Copy link
Copy Markdown

🚨 CRITICAL Supply Chain Risk Detected

This PR contains a pattern that has been used in real supply chain attacks. A maintainer must review the flagged code carefully before merging.

🚨 CRITICAL: Install-hook file added or modified

These files can execute code during package installation or interpreter startup.

Files:

setup.py

Scanner only fires on high-signal indicators: .pth files, base64+exec/eval combos, subprocess with encoded commands, or install-hook files. Low-signal warnings were removed intentionally — if you're seeing this comment, the finding is worth inspecting.

Bounded upstream catch-up: 1734 commits behind origin/main (frozen target
929dd9c). 36 conflicts resolved (84 hunks). The fleet runs fork/main, so
every fork-only fleet-critical system was preserved; upstream's 1546 inbound
commits were absorbed.

ARCHITECTURAL PORTS / SEMANTIC RECONCILIATIONS (the genuine merges, not picks):

- plugins/memory/mem0/__init__.py: TWO incompatible self-hosted mem0
  implementations. Took the fork's 1653-line self-contained client
  (_DirectRestMem0Client + Wave-2 hybrid retrieval + dedup ladder + temporal
  boost + forget/restore + user-id pinning) wholesale; upstream's thin
  delegating __init__ + _backend.py/_oss_providers.py/_setup.py (and their
  tests test_mem0_backend/_setup/_providers/_v3) are a DIFFERENT architecture
  the fork supersedes — removed as orphans. test_mem0_v2.py (UD) accept-deleted
  (coverage lives in fork's test_mem0_remember/_selfhost/_destructive).
  Rationale recorded in docs/sync/review/mem0-resolution-decision.md.

- gateway/run.py hygiene compaction: merged BOTH data-loss guards — fork's
  Issue-8 ABORT guard (don't rewrite the user/asst-only subset on summarizer
  abort) AND upstream's NousResearch#21301 no-rotation guard (don't DELETE the transcript
  when the hygiene agent has no session_db). Rewrite now requires
  (not aborted) AND (rotated OR in-place). Plus fork's F2 restart-loop
  breaker page (_notify_restart_loop_suspended) on upstream's async
  _finalize_shutdown_agents; both sides' os.environ[HERMES_SESSION_KEY]
  removal kept.

- agent/context_compressor.py: fork's per-model threshold re-resolution
  (compaction-thrash fix) threaded BEFORE upstream's max_tokens +
  _compute_threshold_tokens; constructor takes both fork (skew_floor,
  per_model_threshold, codex_gpt55_autoraise, calibration_hard_frac) and
  upstream (max_tokens) params; fork's should_defer removal kept.

- agent/turn_context.py: interleaved upstream's _compression_made_progress
  anti-infinite-loop guard + history rebuild + retry-state resets WITH fork's
  P2 calibrated re-check (note_rough_sent + should_compress_calibrated).

- agent/auxiliary_client.py: fork's route-scoped compaction resilience
  (timeout + max_retries=0 via .with_options) threaded through upstream's
  create_anthropic_message(); fork's atomic provider/model pair + upstream's
  MoA aggregator resolution; both new helper fns kept.

- agent/chat_completion_helpers.py: fork's fallback-announce stack
  (_emit_fallback_announce, _primary_cooldown_seconds, error_context-aware
  try_activate_fallback) + upstream's rewrite_prompt_model_identity (used by
  agent_runtime_helpers); fork's reasoning-effort override + aux-routing
  re-sync; merged fork's stream tail-flush/surrogate-repair WITH upstream's
  interrupt-guard.

- cron/scheduler.py: kept BOTH firing functions — fork's _process_one_job +
  run_job_now (backs `cron run --wait`) AND upstream's run_one_job (the
  external-provider contract: cronjob_tools, web_server, Chronos plugin).
  Folded upstream's correctness fixes (_is_cron_silence_response,
  _summarize_cron_failure_for_delivery, _confirm_adapter_delivery) into fork's
  _process_one_job. tick() fires via _process_one_job.

- agent/agent_init.py: union compression-cfg + max_tokens constructor args;
  took upstream's normalize_tool_schema + NousResearch#47707 nameless-schema poison-guard.

- tools/delegate_tool.py: adopted upstream's lockstep blocklist-derived strip
  set BUT preserved the fork's intent that code_execution is NOT blocked for
  subagents (they already have terminal) — only `delegation` stays composite-
  blocked. Kept fork's child send-origin/cron binding + upstream's
  stream_callback relay.

- hermes_cli/kanban_db.py: union — both model_override (fork per-task model)
  and project_id (upstream) columns + both set_task_model / set_branch_name;
  reconciled the INSERT (21 cols == 21 placeholders == 21 values).

MECHANICAL keep-both / take-upstream / take-fork resolutions:
hermes_state.py (union schema cols last_turn_*/git_*; trigram degrade +
_trigram_available flag + include_trigram param), gateway/session.py (fork
had_any_turn durable signal), gateway/session_context.py (both send-origin +
async_delivery_supported), hermes_cli/config.py (fork fleet-tuned
restart_drain_timeout=180 + F2 breaker config + hygiene limits),
hermes_cli/commands.py (union: undo/redo half-turn + prompt/compose;
slack-via-hermes {credits,version,billing,moa,debug}), hermes_cli/models.py
(both provider_label_oauth + plausibility gate), hermes_cli/backup.py (union
excludes + external-memory-provider backup), agent/conversation_loop.py (fork
error_context + upstream failover-system-message), agent/usage_pricing.py
(fork date-strip/codex-normalize + upstream bedrock, placed in the right fn),
toolsets.py (fork mem0_remember resident + upstream project-tools note),
tui_gateway/server.py (both undo-clear + branch-seed).

Test conflicts: add/add unions where both appended new tests (no name
collisions); test_run_agent.py (DU) accept-deleted (fork split it into
per-shard files); test_update_check.py call_count corrected to 5 empirically
(merged path does fork's HEAD cache-key AND upstream's --is-shallow probe).

LCM vendoring provenance re-validated (PASS). uv.lock + package-lock.json
auto-merged clean. All 15 INV-1 modules import; run_agent/gateway/cli import
end-to-end.
@Kyzcreig
Kyzcreig force-pushed the sync/upstream-2026-06-29 branch from 23d6c3c to 40591f1 Compare June 30, 2026 03:06
@github-actions

Copy link
Copy Markdown

🚨 CRITICAL Supply Chain Risk Detected

This PR contains a pattern that has been used in real supply chain attacks. A maintainer must review the flagged code carefully before merging.

🚨 CRITICAL: Install-hook file added or modified

These files can execute code during package installation or interpreter startup.

Files:

setup.py

Scanner only fires on high-signal indicators: .pth files, base64+exec/eval combos, subprocess with encoded commands, or install-hook files. Low-signal warnings were removed intentionally — if you're seeing this comment, the finding is worth inspecting.

@Kyzcreig
Kyzcreig merged commit 7714f9d into main Jun 30, 2026
36 of 38 checks passed
@Kyzcreig
Kyzcreig deleted the sync/upstream-2026-06-29 branch June 30, 2026 03:13
Kyzcreig pushed a commit that referenced this pull request Jun 30, 2026
…y upstream parity merge

The 2026-06-29 upstream parity merge (PR #119) took upstream's toolsets.py
structure, which silently dropped two fork-only toolsets:

  * messaging -> send_message (agent-callable; the fork's origin-routing work
    #71/#83 is built on it). Upstream deliberately omits an agent-callable
    send_message; this fork re-enables it.
  * moa -> mixture_of_agents (tools/mixture_of_agents_tool.py, fully deleted).

Post-merge resolve_toolset('messaging')/('moa') returned [] with no error, so
on deploy Apollo would silently lose both tools across cli/telegram/discord.
Caught by the config-migration dry-run's post-migration toolset validation.

Restores:
  - toolsets.py: messaging + moa entries (marked FORK-CUSTOM so a future merge
    preserves them)
  - tools/mixture_of_agents_tool.py (self-registers moa via registry.register)
  - agent/display.py: mixture_of_agents formatter + arg-key
  - model_tools.py: moa_tools legacy alias
  - acp_adapter/tools.py: mixture_of_agents in polished ACP tools
  - hermes_cli/tools_config.py: messaging+moa UI rows, moa default-off + env req
  - hermes_cli/config.py: OPENROUTER_API_KEY tools list (+ mixture_of_agents)
  - hermes_cli/tips.py: mixture_of_agents tip

Adds tests/agent/test_fork_custom_toolsets.py — a HARD regression gate that fails
CI if either fork toolset is dropped again (proven: 6/6 pass restored, 6/6 fail
on the broken pre-restore tree). Hardened against the editable-install live-tree
leak so a deleted module can't false-pass via a sibling install.
Kyzcreig pushed a commit that referenced this pull request Jul 1, 2026
Distills the 5 CI-gate traps that gave false local greens during the
2026-06-29 upstream parity sync (PR #119): live-tree submodule leak,
pinned gitleaks version + config auto-discovery, contributor-check's
narrow noreply auto-skip, the setup.py supply-chain maintainer gate,
and branch-protection required-context drift. Companion to the existing
fullsuite-final-delta-classification.md.
@Kyzcreig
Kyzcreig restored the sync/upstream-2026-06-29 branch September 21, 2026 10:32
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.