sync: defer 599 commits (refreshed) — real tui_gateway server.py collision + contract/cache surfaces + unported pets/projects/learn/oneshot/MoA/verify/subagent-status - #19
Closed
alt-glitch wants to merge 600 commits into
Conversation
Prevents stage2-hook.sh recursive chown from following a symlinked $HERMES_HOME/home (or profiles/cron) and destroying the host user's home directory. Also guards top-level state-file chowns and refuses first-boot seeding through symlinks. Fixes NousResearch#52781. Co-authored-by: harjoth <harjoth.khara@gmail.com>
…ousResearch#48415) (NousResearch#52760) CredentialPool._sync_device_code_entry_to_auth_store rotated single-use OAuth refresh tokens but wrote the new chain only into the active profile store. When a profile resolves a grant from the global-root fallback (read_credential_pool, NousResearch#18594) and the pool then refreshes it, root was left holding a now-revoked refresh token — every other profile reading the stale root grant subsequently died with refresh_token_reused / invalid_grant once its access token expired. This is the credential-pool analog of NousResearch#43589 (which fixed the non-pool xAI refresh path in _save_xai_oauth_tokens). Detect the read-from-root case (profile lacks its own providers.<id> block) BEFORE the profile save and, after it, write the rotated chain back to the global root via a best-effort, seat-belted write-through. A profile that genuinely shadows root (owns the block) is untouched; classic mode (profile == root) is a no-op; a failed root write never breaks the profile's own save. Covers openai-codex (reported), xai-oauth, and nous through the shared sync path.
…laceholder platforms (NousResearch#52831) The scale-to-zero idle watcher never started on a correctly-opted-in, relay-only instance, so the gateway never ran its idle decision, never called go_dormant(), and never sent going_idle to the connector. Fly's autostop still suspended the machine on traffic-idle, but the connector never flipped the instance to buffered-only — so an inbound DM took the live delivery path, found no live session for the suspended machine, and was dropped fail-closed with no wake poke. The machine slept and never woke. Root cause: _scale_to_zero_should_arm() passed list(config.platforms.keys()) to messaging_is_relay_only_or_absent(). config.platforms is pre-seeded with a DISABLED placeholder PlatformConfig for every known platform (telegram, discord, slack, matrix, …), so the key set is always the full ~20-entry catalog regardless of what the instance actually runs. The relay-only check discarded "relay", saw the disabled placeholders as live direct-socket platforms, and returned False — so should_arm() was False and the watcher was never created. Verified live on a staging instance: config.platforms keys = [telegram, discord, slack, mattermost, matrix, relay] with only relay enabled=True; should_arm() = False. Fix: filter config.platforms to ENABLED entries before the relay-only check, mirroring the adapter-connect loop which already gates on `if not platform_config.enabled: continue`. This arms off the same notion of "active platform" the rest of start() already uses — no parallel concept. Also add a one-line not-armed diagnostic: when an instance IS opted in (the HERMES_SCALE_TO_ZERO stamp is set) but the watcher still doesn't arm, log why (relay_only_or_absent, the enabled platforms, wake_url present/missing). A non-opted instance stays silent. The arm path previously logged only on success, so a failed arm was invisible. Tests: the existing pure-helper tests passed bare names so they never exercised the call site that feeds the placeholder-laden config. Add behaviour-contract tests against the REAL _scale_to_zero_should_arm with a realistic config.platforms (relay enabled + others disabled). The F25 regression test (relay-only + disabled placeholders must arm) and the no-platform case are RED without this fix, GREEN with it; the genuinely-enabled-direct-platform / not-opted-in / no-wake-url cases stay correctly non-arming so the filter can't over-broaden. Wake mechanism itself verified healthy independently (direct wakeUrl GET resumed a suspended staging instance in 1.15s, clean resume signature).
…5572-8m77) The email adapter authorized senders entirely off the From: header, which is attacker-controlled and unauthenticated by IMAP. An attacker could forge From: an-allowlisted-address and pass both the adapter's EMAIL_ALLOWED_USERS pre-filter and the gateway's allowlist authz (both key on the same spoofable sender_addr), getting unauthorized commands executed by the agent. Verify the From: domain against the trusted Authentication-Results header the receiving mail server stamps (SPF/DKIM/DMARC) before trusting it for authorization. Enforced only when an allowlist is in effect and allow-all is off — fail-closed. Operators whose server does not stamp the header can opt out via platforms.email.require_authenticated_sender: false (or EMAIL_TRUST_FROM_HEADER=true).
…s gateway history (NousResearch#52798) A readable state.db can still reject every message write through the messages_fts* triggers when the FTS5 index is corrupt: base-table reads and PRAGMA integrity_check pass, but INSERT INTO messages fails with 'database disk image is malformed'. The gateway reloads conversation_history from disk each turn, so a silently-failed write hands the next turn stale/empty history even though the same cached AIAgent still holds the live transcript — causing immediate same-session amnesia. (NousResearch#50502) - hermes_state.py: _db_opens_cleanly() now drives a rolled-back message write through the FTS triggers, so write-only corruption (which the read-only probe reported healthy) is detected. repair_state_db_schema() gains an in-place FTS5 'rebuild' strategy (tier 0) before the dedup/drop tiers, plus an already_healthy short-circuit. Both 'hermes sessions repair' and 'hermes doctor' route through these, so the fix covers the whole class. - hermes_cli/doctor.py: the state.db check runs the write-health probe even on the success (readable) path and repairs in place with --fix. - gateway/run.py: _select_cached_agent_history() prefers the cached agent's longer live _session_messages over a shorter persisted transcript, so an FTS write failure can't wipe in-session context. - tests: regressions for write-health detection, in-place repair preserving rows + resuming writes, the already_healthy shortcut, and the gateway guard. Combines the approaches from NousResearch#50504 (@0-CYBERDYNE-SYSTEMS-0, issue author), NousResearch#52165 (@davidgut1982), and NousResearch#50576 (@trevorgordon981).
When the primary provider raises AuthError (e.g. expired OAuth token), _make_agent now walks the configured fallback_providers/fallback_model chain before giving up — matching the behavior that cron/scheduler.py and cli_agent_setup_mixin.py already have. Fixes NousResearch#47627
After a prolonged outage the in-process network-error ladder escalates to fatal and GatewayRunner._platform_reconnect_watcher rebuilds a fresh adapter that reconnects through the bootstrap path. That path called start_polling(drop_pending_updates=True), discarding every update Telegram queued during the outage — all messages sent while the bot was down were silently lost. The in-process ladder and 409-conflict handler already passed drop_pending_updates=False; only bootstrap did not distinguish a cold first boot from a reconnect. Thread an is_reconnect signal from the watcher through _connect_adapter_with_timeout into adapter.connect(). The base BasePlatformAdapter.connect() gains a keyword-only is_reconnect=False so every adapter inherits a tolerant signature (no per-platform breakage when the runner forwards the kwarg). Telegram translates is_reconnect into drop_pending_updates=not is_reconnect on both the polling and webhook bootstrap calls. Cold boot still drops the stale queue; a watcher reconnect preserves it. Fixes NousResearch#46621. Co-authored-by: annguyenNous <annguyen@nousresearch.com> Co-authored-by: kyssta-exe <kyssta-exe@users.noreply.github.com> Co-authored-by: Kewe63 <Kewe63@users.noreply.github.com>
…h#52848) * feat(kanban): typed block reasons + unblock-loop breaker Stops the kanban blocked-task loop: a worker blocks a task, a cron unblocks it, the worker re-blocks for the same reason, repeat forever. block_task now takes a typed kind and a persistent block_recurrences counter on the tasks table: - kind=dependency routes to todo (parent-gated, auto-resumed), never the human 'blocked' bucket a cron would keep unblocking. - needs_input/capability/transient/untyped land in blocked; each same-cause re-block after an unblock increments block_recurrences, and at BLOCK_RECURRENCE_LIMIT (default 2) the task routes to triage for a human instead of blocked. - unblock_task no longer resets block_recurrences (the amnesia that let the loop run unbounded); complete_task clears it on success. Wired through the worker kanban_block tool (new kind arg) and the hermes kanban block --kind CLI flag, both reporting where the task actually landed. Docs + 11 new tests; 536 existing kanban tests green. * test(kanban): make second-block notify test use a distinct block cause test_notifier_second_blocked_delivers blocked the same task twice with the same (untyped) reason, which now trips the new unblock-loop breaker and routes the second block to triage instead of blocked — so only one 'blocked' notification fired. The test's actual intent is that TWO distinct block cycles each notify; give the two cycles different kinds (needs_input then capability) so they're genuinely separate blocks. The same-cause loop→triage path is covered by test_kanban_block_kinds.py.
WSLg bridges clipboard text but not images — pull host screenshots via PowerShell. Disable titleBarOverlay on plain Linux; gate overlay width per platform in titlebar-overlay-width.cjs.
Live-measure WCO width in the renderer, drop the right rail below the titlebar band, and re-enable GPU compositing under WSLg when /dev/dxg is present.
Use a transparent native overlay so renderer chrome shows through the min/max/close band. Sync window pre-paint bg to the computed chrome mix.
Stopping a turn while the model is streaming (stop/esc to redirect) raised InterruptedError, set final_response to the throwaway "waiting for model response" sentinel, and persisted messages WITHOUT the assistant text that was already streamed to the screen. The next turn then had no record of the half-finished reply, so the model appeared to "forget" what it just said. Recover the on-screen text from _current_streamed_assistant_text in the InterruptedError branch and append it as the assistant turn (and surface it as final_response). The metadata sentinel is kept only when nothing was streamed yet, preserving the ACP/client suppression behavior. Completes the partial-stream recovery from 397eae5 (which wired the same _current_streamed_assistant_text salvage into the connection-failure twin but missed the user-interrupt path). The lossy handler dates to c98ee98.
…p-fixes fix(desktop): WSL2 clipboard paste, titlebar layout, HMR survival, and GPU acceleration
Force redact_sensitive_text(force=True) on the browser_type text arg so recognized credentials (API keys, tokens, JWTs) are masked in tool progress, previews, callbacks, and return payloads even when the global security.redact_secrets opt-out is set — a typed credential reaching chat history is a security boundary, not log hygiene. Normal typed text matches no pattern and stays fully readable for debuggability. Tests assert the API-key-shaped secret is masked across every surface and that normal text passes through unchanged.
…ration The salvaged NousResearch#51875 added a background-review write guard in skill_manage that refuses mutations to skills.external_dirs skills — but it only fires when is_background_review() is true. The curator's LLM review fork ran with the default _memory_write_origin='assistant_tool', so the guard never triggered during the exact curation pass it exists to protect against (NousResearchGH-47688). - Set _memory_write_origin='background_review' on the curator review fork so turn_context binds it onto the write-origin ContextVar and the guard fires. - Add a regression test asserting the fork runs under the background_review origin (the invariant linking the fork to the guard). - AUTHOR_MAP: map yu-xin-c for the salvaged commit.
…upt-partial-reply fix(interrupt): keep partial streamed reply when stopped mid-response
…+ non-stream detectors Wire get_reasoning_stale_timeout_floor() into both stale detectors so known reasoning models (Nemotron 3 Ultra, OpenAI o1/o3, Opus 4.x thinking, DeepSeek R1, Qwen QwQ, Grok reasoning) tolerate multi-minute thinking phases instead of the upstream gateway idle-killing the socket (BrokenPipeError) before first token. Applied as max(default, floor) — never overrides explicit user config, never lowers an existing threshold. The reasoning_timeouts.py allowlist module already landed on main via NousResearch#52795, so this salvage carries only the wiring + tests (the duplicate module and the stale-base MoA reverts from the original PR branch are dropped). Salvaged from NousResearch#52238. Fixes NousResearch#52217.
…2-fuzzy-boundary fix(fuzzy-match): preserve boundary space after whitespace-normalized match (NousResearch#52491)
Ensure TUI/desktop stop targets the actual conversation thread and cancels any queued next prompt, including the lazy agent-start window, so a stopped session cannot keep running or restart itself.
Hold Alt/Option and scroll over the mascot to resize it (same on Mac and Windows); the modifier keeps a plain scroll passing through to the page. The gesture drives the same `display.pet.scale` path as the settings slider. The popped-out overlay grows its OS window to fit the pet at any scale (anchored bottom-center) so the sprite is never clipped by the window edge, and the in-window pet re-clamps against its actual size so growing near an edge can't crop it. Also makes the overlay click-through per-pixel: only solid sprite pixels (plus bubble / mail button) are interactive, transparent margins pass clicks through.
…terrupt-queued fix(tui-gateway): make stop interrupt queued turns
Alt+wheel now scales about the pixel under the pointer instead of growing from a corner, so the pet stays put under the cursor instead of running away. In-window shifts its top-left; the overlay repositions its OS window (cursor-anchored on wheel, bottom-center for slider-driven changes).
…gesture feat(desktop): Alt+wheel to scale the pet, never cropped
…-map-tranquil-flow chore: add Tranquil-Flow to AUTHOR_MAP for auxiliary base_url salvage
…a silent empty turn (NousResearch#53912) When a Telegram attachment download/cache fails (typically a transient httpx.ConnectError to Telegram's CDN), the except handler logged a warning and fell through to handle_message() with empty media and no text — the user thought the file was delivered, the agent saw a content-less turn with no signal an attachment was attempted, and the only record was a buried log line. Adds _surface_media_cache_failure(): replies to the user in Telegram so they know to retry, and appends an agent-visible notice to event.text via the existing _append_observed_note channel so the agent knows an attachment was attempted and failed. No new event fields (structured-event refactor is out of scope per NousResearch#23045). Wired into all five cache-failure sites — photo, voice, audio, video, document — since they shared the identical silent fall-through. Bug 1 from NousResearch#23045 (unsupported types routed as fake user messages) no longer exists on main: the document handler now accepts any file type, so there is no rejection branch to fix. Closes NousResearch#23045
read_claude_code_credentials() previously returned the macOS Keychain entry as soon as one existed, even if its OAuth token was already expired. Callers then ran is_claude_code_token_valid() on the result and got False, so resolve_anthropic_token() returned None — surfacing the misleading 'No Anthropic credentials found' error even when ~/.claude/.credentials.json held a perfectly valid token. Now reads both sources and prefers the non-expired one. When both are valid (or both expired), prefers the later expiresAt so any subsequent refresh uses the freshest refresh_token. Adds TestReadClaudeCodeCredentialsDesync covering the four reconciliation cases. The existing 'keychain wins' priority test still passes because both fixtures share the same expiresAt and the tiebreaker is >=.
…cing refresh Claude Code OAuth refresh tokens are single-use; Claude Code refreshes on its own schedule, so by the time Hermes notices an expired token Claude Code may have already rotated it. Re-read live credential sources first and adopt a valid token rather than POSTing a possibly-stale refresh token. Ports the _refresh_oauth_token hardening from PR NousResearch#40107 (chazmaniandinkle) on top of the keychain/file reconciliation from PR NousResearch#21112 (nodejun). Adds AUTHOR_MAP entry for nodejun.
…earch#24996) (NousResearch#53909) When every provider in the fallback chain fails non-retryably back-to-back (e.g. HTTP 400/402/429 across distinct providers), the within-turn walk is already bounded — _fallback_index advances monotonically and the loop aborts when the chain exhausts. The damaging mode is cross-turn: restore_primary_ runtime resets _fallback_index=0 every turn, so a client that re-submits immediately replays the entire chain, re-marshaling the full (potentially 80k-token) context once per provider every turn with no throttle on the non-rate-limit path. On constrained hosts this exhausts memory/swap. Rate-limit/billing failures already arm a 60s cooldown via _rate_limited_until; the gap was the non-rate-limit case. Now, when the chain exhausts on a non- rate-limit failure with a non-empty chain, arm a short (5s) cooldown on the same _rate_limited_until gate (max(), never shrinking an existing window). The next turn's restore stays gated and does NOT reset the index, so the chain isn't replayed until the cooldown clears. No new state, no thread sleep, no false-trip on legitimately long chains (those walk normally within a turn). Tests: tests/run_agent/test_24996_fallback_exhaustion_cooldown.py
…ate (NousResearch#26080) A persistent upstream 401 on a single-entry OAuth pool (common for Claude Max subscribers) made the credential-pool recovery spin forever: try_refresh_current() re-mints a fresh token and reports success on every 401, so recover_with_credential_pool returned True and the retry loop continue'd without ever incrementing retry_count or reaching the auth-failover block. The configured fallback_model never activated and the agent appeared to hang. Cap consecutive successful same-entry refreshes (keyed by provider + pool-entry id) at 2; once exceeded, treat the credential as unrecoverable and return not-recovered so the loop falls through to _try_activate_fallback. The 429/billing paths already rotate-or-fall-through correctly (mark_exhausted_and_rotate returns None on a single entry), so only the auth-refresh branch needed the cap. Co-authored-by: Hermes Agent <hermes@nousresearch.com>
…er cap to 600s The Anthropic SDK clients were built without max_retries, so the SDK default (max_retries=2) retried 429/5xx with its own backoff that ignores Retry-After — double-retrying inside hermes's outer loop and burning request slots against a bucket that won't refill for minutes. Set max_retries=0 on all Anthropic/AnthropicBedrock client constructions so the outer conversation loop (which already honors Retry-After) owns retry. Also raise the Retry-After cap in the conversation loop from 120s to 600s. Anthropic Tier 1 input-token buckets reset in ~171s, so the 120s cap made hermes retry before the reset window and re-trip the limit. Refs NousResearch#26293
…e rate-limit loop Same bug class as the Anthropic fix (NousResearch#26293): the OpenAI/aggregator client is built without max_retries, so the SDK default of 2 applies. The SDK's own 1-2s backoff ignores Retry-After and retries inside hermes's outer conversation loop, burning request slots against a rate-limited bucket. Set max_retries=0 at the single create_openai_client chokepoint (covers init, switch_model, recovery, restore, request-scoped). auxiliary_client builds its own clients and is not wrapped by the loop, so it keeps SDK retries.
…ncommon image formats
A document attached alongside an image in the same Discord message was
swept into the vision pipeline and 400'd the whole turn ("Could not
process image"), and was simultaneously never surfaced to the agent as a
readable file. Restores the "any file type works" contract for mixed
messages and fixes the HTTP 400.
Bug 1 — mixed attachments: the inbound routing loop keyed image/audio/video
classification off the message-level type (PHOTO/VOICE/AUDIO), so a doc in
a PHOTO message landed in image_paths and poisoned the vision call. The
document context-note path was gated on message_type == DOCUMENT, so that
same doc never reached the agent at all. Now classification is
per-attachment (trust each attachment's own MIME; fall back to the
message-level type only when MIME is unknown), via shared _event_media_is_*
helpers used by both _build_media_placeholder and the main inbound loop.
The document note now fires for any non-image/audio/video attachment
regardless of message-level type.
Bug 2 — uncommon formats: AVIF/HEIC/BMP/TIFF/ICO produced the same generic
400 because providers only accept PNG/JPEG/GIF/WEBP. image_routing now
transcodes those to PNG via Pillow before declaring media_type, skipping
cleanly (logged) if Pillow/plugins are missing. SVG is vector — Pillow
can't rasterize it — so it's skipped rather than transcoded.
Closes NousResearch#25935.
Co-authored-by: LeonSGP43 <cine.dreamer.one@gmail.com>
Co-authored-by: cypres0099 <74935762+cypres0099@users.noreply.github.com>
…/terminal tools A flaky external probe in a tool's check_fn (e.g. check_terminal_requirements running `docker version` with a 5s timeout, momentarily timing out under load) would return False for a single get_tool_definitions() call. Because file tools delegate their check_fn to the terminal check, that one flake silently stripped read_file/write_file/patch/search_files AND terminal from whatever agent was being constructed at that instant — most visibly a delegate_task subagent, which then reported "Tool read_file does not exist". This explains both the intermittent (~80% success) user-session failures and the deterministic cron failures in NousResearch#21658 / NousResearch#5304. The existing _check_fn TTL cache made this worse: it cached the transient False for the full 30s window, poisoning every subagent spawned in that span. Fix: remember the last time each check_fn returned True; when a fresh probe fails within a short grace window of that success, treat it as a flake — serve the last-good True and do NOT cache the failure (so the next call re-probes). A failure with no recent success, or past the grace window, is honored normally so a backend that genuinely went down stops advertising its tools. Probe failures now log at WARNING regardless of quiet mode, making the previously-silent tool loss diagnosable in subagent (quiet) sessions. Co-authored-by: Stuart Horner <5261694+djstunami@users.noreply.github.com>
…ousResearch#26211) Root cause: when the terminal environment (`_active_environments` entry) is cleaned up and re-created during a long conversation, the new environment always starts with the default config CWD (typically `~/.hermes/hermes-agent`) instead of preserving the user's last-known working directory. Subsequent relative-path writes (`write_file`, `execute_code`, shell commands) silently land in the default CWD, making files appear to be "created but absent." Fix: add `_last_known_cwd` dict that preserves the old environment's CWD before the stale cache entry is invalidated. When a new environment is created for the same task_id, we check `_last_known_cwd` first and use the preserved CWD instead of the config default. Changes: - tools/file_tools.py: add `_last_known_cwd` dict, save CWD before stale cache invalidation, restore CWD on env recreation - tests/tools/test_file_tools.py: add `TestLastKnownCwd` with 2 tests verifying CWD preservation and fallback behavior Fixes NousResearch#26211
…ousResearch#26211) Belt-and-suspenders on top of the cherry-picked cwd-preservation fix: - Proactively mirror every live terminal cwd into _last_known_cwd on each successful read, so the durable anchor survives even when the cleanup thread pops both _file_ops_cache and _active_environments before _get_file_ops' stale-cache save branch can fire. - Fall back to _last_known_cwd in _authoritative_workspace_root. write_file_tool resolves the path (via _resolve_path_for_task) BEFORE _get_file_ops rebuilds the env, so restoring only the rebuilt env's cwd was insufficient — the resolution that decides where the file lands runs first. This closes that gap. The local env's persisted _cwd_file can't serve this role: it's keyed by a random per-session uuid and deleted on cleanup (the same cleanup that triggers the bug). The in-memory _last_known_cwd registry is the durable anchor instead. Adds a real-IO E2E regression (TestSilentFileMisplacementE2E) exercising the actual write_file_tool path after env cleanup.
…ousResearch#26211) The durable _last_known_cwd anchor is keyed by the shared 'default' container, so a non-owning worktree session could inherit the owning session's cwd through it — breaking the wrong-worktree-routing fix (test_file_tools_cwd_resolution:: test_resolution_routes_to_resolving_sessions_worktree). Reorder _authoritative_workspace_root so the session-specific registered cwd override (keyed by raw session id) is checked BEFORE the shared-container _last_known_cwd fallback. A non-owning session now resolves into its own registered worktree; the durable anchor only fills in when there's no session-specific override (the NousResearch#26211 single-session case). Adds a regression test covering the owner-mirrors-then-other-session-resolves interaction.
…ousResearch#26656) The Discord adapter could enter a silent zombie state after a network outage / proxy stall: the process is alive, _client looks open, but the underlying socket is dead. discord.py's WebSocket reconnect never sees a RST through a wedged proxy/NAT, so client.start() spins forever without exiting — which means the bot-task done callback (which only fires on task completion) never trips either. The bot stays "offline" in Discord until a manual `hermes gateway restart`. Reported offline for 13-17h. Adds an out-of-band REST liveness probe in DiscordAdapter. Every `discord.liveness_interval_seconds` (default 60s) the adapter issues a cheap fetch_user(bot_id) — the same REST path as message delivery, so it fails when the proxy/NAT is wedged. After `discord.liveness_failure_threshold` consecutive failures (default 3) the probe closes the wedged client and surfaces a retryable fatal error, which trips the gateway's existing _platform_reconnect_watcher and rebuilds the adapter. Operators disable it by setting either knob to 0. Config lives in config.yaml (discord.liveness_*) per the .env-is-secrets policy; _apply_yaml_config bridges it to internal env vars the adapter reads, matching the existing HERMES_DISCORD_TEXT_BATCH_* pattern. Co-authored-by: Hermes Agent <agent@nousresearch.com>
… OpenAI client interruptible_streaming_api_call() has three connection-pool cleanup sites that called _replace_primary_openai_client() unconditionally. For api_mode=anthropic_messages this has two consequences: 1. _replace_primary_openai_client() fails (OPENAI_API_KEY unset on Anthropic-only configs), so dead connections are never purged. 2. The stale-stream detector's outer-poll site (L1977) is the only mechanism that can interrupt the worker thread while it blocks in for event in stream:. Because the Anthropic client is never closed, the thread stays blocked until the 900 s httpx read-timeout fires, producing a visible 15-minute hang for Telegram/gateway users on claude-opus-4-7. Fix: mirror the existing interrupt-path pattern (L1989-1997) at all three cleanup sites — if api_mode == "anthropic_messages", call _anthropic_client.close() + _rebuild_anthropic_client() instead of _replace_primary_openai_client(). _rebuild_anthropic_client() handles both direct Anthropic and Bedrock-hosted Claude correctly, unlike the inline build_anthropic_client() calls in open PR NousResearch#14430. PR NousResearch#14430 (open) covers only the outer stale-detector site (L1977). PR NousResearch#23678 (open) covers only the inner retry sites (L1774, L1833). This PR covers all three sites and uses _rebuild_anthropic_client() for Bedrock parity. Fixes NousResearch#28161
…ld path The existing test_anthropic_stream_parser_valueerror_retries_before_delivery asserted mock_replace.call_count == 1 — i.e. it passed precisely because the buggy OpenAI rebuild was invoked on the Anthropic path. Repoint it to assert the corrected close+rebuild-Anthropic behavior (NousResearch#28161).
Fixes NousResearch#27354 Root cause: called during init (or by any code path that saves ) wrote injected schema defaults into config.yaml as if the user had authored them. Two fix layers: 1. now only injects when the user actually set somewhere (root or agent). A user who never set keeps it absent, so 's explicit-path detection won't treat it as user-authored. 2. gains a parameter and a new pass that removes keys matching unless those paths were explicitly present in the **raw** (pre-normalization) config on disk. Explicit-path detection uses on *before* any normalisation runs — preventing injected-in defaults from being mistaken for user-set values. All migration and edit-config call sites pass to preserve their intentional default-seeding behaviour. New helpers: - — collects leaf-key paths from a raw dict - — removes keys matching schema defaults Test coverage: 4 new regression tests (59 total, all passing).
The salvaged NousResearch#27354 fix made save_config strip schema-default leaves by default. Five migration sites added to main after the PR was authored still called bare save_config(config) and intentionally materialize a (often default-valued) key: model_catalog.ttl_hours, write_approval, curator.consolidate, agent.verify_on_stop, and the suspicious-MCP-server disable. Pass strip_defaults=False so those one-time deliberate writes survive, matching the opt-out the PR applied to the other migrations.
…top cross-profile flap (NousResearch#29092) Two profile gateway services sharing the default ~/.hermes resolve the takeover marker to the same path. A --replace from profile B could land in profile A's marker, match on PID + start_time by coincidence of a shared PID namespace, and make profile A exit 0 — only to be revived by systemd Restart=always, which races the replacer again, flapping indefinitely. write_takeover_marker now stamps replacer_hermes_home; the shared consume path rejects markers written under a different HERMES_HOME and leaves them in place for the correct profile. Absent field (older markers) is treated as same-home, so single-profile and mixed old/new deployments are unaffected. Salvaged from NousResearch#31414 by @CryptoByz onto current main (branch was ~3962 commits behind; the consume function had since been refactored for issue NousResearch#34597). Co-authored-by: CryptoByz.
…NousResearch#29285) `resolve_provider("auto")` checked `auth.json` `active_provider` BEFORE the config.yaml `model.provider` and env-var API-key checks. So a user who was OAuth-logged-into one provider (e.g. Anthropic) but had set an explicit `model.provider` or exported an API key (e.g. `OPENAI_API_KEY`) was silently routed to the stale OAuth provider — the override was invisible and surprising. Reorder the auto-path so explicit intent wins (the order the issue asks for): 1. explicit CLI api_key/base_url 2. config.yaml `model.provider` (safety net — see below) 3. OPENAI_API_KEY / OPENROUTER_API_KEY env 4. OpenRouter credential pool 5. provider-specific API-key env vars 6. auth.json `active_provider` (OAuth) ← demoted to last-resort 7. AWS Bedrock credential chain 8. error `active_provider` is still honored — it's just a last-resort fallback chosen only when the user expressed no other preference, instead of overriding one. The normal chat/gateway/TUI/ACP/status path already resolves config.provider upstream in `resolve_requested_provider()` before "auto" is reached, so this duplicate config check is the safety net for the lone direct caller (`main.py` `resolve_provider("auto")`) and any future bypass. Because every surface funnels through this one resolver, the fix propagates everywhere with a single edit — no sibling path re-implements precedence. Also add a one-shot WARN when resolution lands on `active_provider` while a populated `model` config dict lacks a `provider` key — surfacing the silent override the issue reported without breaking first-install. Synthesizes the two competing PRs: NousResearch#29615 (LifeJiggy — config-before-auth + the silent-override framing) and NousResearch#29809 (Minksgo — the env-before-auth reorder). NousResearch#29809 could not be merged directly (bundled unrelated, un-opt-in cost-tagging telemetry); its reorder idea is incorporated here and credited. Tests: tests/hermes_cli/test_provider_precedence.py — config/env beat stale OAuth, OAuth still used as last resort, explicit request short-circuits, WARN fires on silent fall-through. Full provider-resolution suites: 374 passed. Fixes NousResearch#29285 Co-authored-by: LifeJiggy <141562589+LifeJiggy@users.noreply.github.com> Co-authored-by: Minksgo <153416856+Minksgo@users.noreply.github.com>
_restore_primary_runtime restored the construction-time api_key snapshot and never consulted the credential pool. After the pool rotated away from a revoked/exhausted entry mid-session, every new turn restored the dead key, re-failed instantly, burned the remaining entries, and fell through to cross-provider fallback. After restoring the snapshot, re-select the pool's current best entry and swap the live credential in via _swap_credential (which already rebuilds the OpenAI/Anthropic client, reapplies base-url headers, and carries the NousResearch#33163 base_url / OAuth-detection fixes). Falls back to the snapshot key when the pool is absent, empty, or the entry has no usable key. Salvaged from NousResearch#25206 onto current main: the original targeted the pre-refactor monolithic method in run_agent.py; the logic now lives in agent/agent_runtime_helpers.py and is collapsed onto _swap_credential instead of re-inlining the client rebuild. Fixes NousResearch#25205
…ldown tests Three CI flakes hit while landing the credential-pool restore fix; all three were timing/wall-clock races in the tests, not product bugs (each passes locally and the assertions are correct): - test_entire_tree_is_sigkilled_not_just_parent: _terminate_host_pid SIGKILLs synchronously, but the test's 4s budget after a 1s in-function SIGTERM grace left almost no slack for the kernel to tear down 3 processes + reparent the children to zombies under loaded-CI scheduling. Widen the wait to 15s and make the liveness predicate tolerant of vanished-pid / zombie races. The assertion never weakens: every tree member must end up dead or zombie. - test_session_resume_follows_compression_tip: appended messages got time.time() timestamps (~now) while the test forced session started_at into the past, so the get_compression_tip MAX(m.timestamp) tiebreaker depended on wall-clock ordering. Pass explicit, well-separated message timestamps so the chain resolution is deterministic by construction. - test_non_retryable_exhaustion_arms_cooldown: asserted the short (5s) exhaustion cooldown with a tight +1.0s slack, which false-fails when wall-clock jitter between the 'before' snapshot and the cooldown computation exceeds a second on a loaded runner. Widen to +30s — still cleanly below the 60s rate-limit window it must distinguish from.
…_gateway/server.py + tests) -- handoff for glitch Refreshed defer snapshot at upstream tip 88b3d86. 599 commits behind. Live sid/opentui branch is UNTOUCHED. Resolve the conflicts and the engine ports before this can advance. See PR description for the full breakdown.
alt-glitch
force-pushed
the
sync/defer-20260627-033117
branch
from
June 28, 2026 03:32
9518426 to
93491a9
Compare
Owner
Author
|
Superseded by a refreshed defer (now 743 commits; +144 since this one). Same root blocker (the session.list handler collision in the gateway server) plus more upstream history. Closing this stale one in favor of the refreshed branch/PR. The live |
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.
sync: defer 599 commits (refreshed) — real tui_gateway/server.py collision + contract/cache surfaces + large unported engine ports
Status: DEFER. The live
sid/opentuibranch is UNTOUCHED — glitch's install keeps running on its current tip.This refreshes the standing defer to the current upstream tip
88b3d8638e83.The gap grew from ~545 to 599 commits (54 new upstream commits since the
last refresh — mostly agent-loop credential/fallback hardening, MoA
render/routing fixes, a Windows console-popup batch + its revert, and
config-defaults fixes).
The branch head is a WIP merge commit with conflict markers left intact as a
reviewable handoff — it is NOT mergeable as-is and must NOT be fast-forwarded
onto
sid/opentui. Resolve the items below first.Why this is deferred (multiple independent triggers)
1. Real, non-additive content collision in
tui_gateway/server.pyFour conflict regions; three are genuine same-line collisions (non-empty diff3
base or mutually-exclusive implementations of the same hook), which is outside
the auto-resolvable additive-test-file shape:
_LONG_HANDLERSset (~L181) — additive on its own (glitch addsmodel.options; upstream adds the completion/pet RPCscomplete.path,complete.slash,llm.oneshot,pet.*). Keep-both is fine here, but itrides along with the harder regions below.
_wire_callbacks(~L3798) — glitch dropped thetimeout=120on the sudocallback; upstream kept it AND added
set_project_workspace_callback(...).Needs a deliberate merge (keep upstream's project callback, decide the sudo
timeout).
session.search/list path (~L5148) — hard collision. glitch rewrotethis block into a paginated/filtered/multi-source scanner with a scan-cap;
upstream independently added
order_by_last_active=Trueto the samelist_sessions_richcall on the pre-rewrite line. The two edits overlap onthe same code; upstream's ordering intent must be folded into glitch's
rewritten query path by hand.
title_callback=lambda _t: _emit_title_refresh(sid)(session.info refresh forwindow-title chrome); upstream wires
title_callback=that emits asession.titleevent for live sidebar rename. Mutually-exclusiveimplementations of the same hook — needs a human decision (likely a hybrid
that both refreshes info and emits the title).
Plus the additive test-file conflict in
tests/test_tui_gateway_server.py(keep-both once the source side is settled).
2. Contract / cache / role-alternation surfaces touched (always-defer)
The gap touches several
!defersurfaces the maintainer never auto-merges:agent-loop-core conversation/compression/interrupt paths, in-place compression
default flip, verify-on-stop agent-loop changes, MoA core routing through real
provider routes, and tui_gateway contract changes. These need a human eye on
cache-prefix stability and strict role alternation.
3. Large unported engine work (Ink/gateway → OpenTUI engine)
Substantial new features land on the Ink TUI / gateway that the OpenTUI engine
does not mirror and that are too big/ambiguous to safely auto-port this tick:
animated egg, hatch FX), generation RPCs + gallery, OpenRouter/Nous image
backend, remix flow. New event families + net-new components.
RPC, kanban↔worktree linking, desktop coding rail / review IPC, per-session
worktree cwd isolation. New gateway contract + sidebar surfaces.
bounding.
model labelled blocks in TUI/desktop, picker integration.
verify-on-stop (later defaulted off) — agent-loop posture changes.
"resumes when subagent finishes" segment.
fast session switching — tui_gateway behavior the engine consumes via
boundary/.Each is a real port (often a new event family or net-new Solid components), not
the additive-variant-on-existing-scaffold shape that's auto-handleable.
What I tried
git merge upstream/mainin a throwaway worktree → confirmed thepredicted conflicts (
tui_gateway/server.py,tests/test_tui_gateway_server.py).server.pyregions → three are real same-linecollisions, not additive → outside the auto-resolve envelope.
always-defer triggers fire (real non-test collision +
!defersurfaces +large unported features), so the verdict is defer regardless of gate result.
What glitch needs to decide
server.pyregions (the session.search query path and theauto-title callback are the two that need real thought; the rest are
keep-both / minor).
verification, subagent-status) to port into the OpenTUI engine vs. leave
Ink-only, and port them deliberately.
npm run check+build.mjs) + the Python gate ontouched
tui_gateway//agent/modules before fast-forwardingsid/opentui.Branch head:
93491a913(WIP merge, markers intact). Base:sid/opentui@
4045c7957. Upstream tip merged:88b3d8638e83.