Salvage of #55614 by @kartik-mem0 (mem0 maintainer). Adds a SelfHostedBackend
that talks to a self-hosted Mem0 Docker server over httpx (X-API-Key auth,
/search + /memories routes), gated behind `host`. Also folds in the mem0
research-team recall tuning that rides with it: rerank defaults to false across
all modes, the mem0_list tool is removed (5->4 tools), search guidance is
de-shouted, and self-hosted get_all reports the true stored total (#52921).
Supersedes the self-hosted portion of #52487 (@liuhao1024, first-submitted).
Closes#52478Fixes#52921
Review findings on the salvaged shim: (a) OpenAI callers may pass stop as
a bare string but Converse's stopSequences requires a list — normalize;
(b) call_llm(stream=True) (MoA aggregator) can reach this client and the
shim silently returned a complete response — keep that behavior (the
streaming consumer's got-final-object path downgrades gracefully) but log
it, and log dropped tool_choice, instead of silently ignoring both.
+2 regression tests.
Follow-up to the salvage of #60217 by @xxxigm.
Auxiliary Bedrock resolution always used the Anthropic Bedrock SDK, which
only works for Claude foundation-model IDs. Non-Claude models such as
openai.gpt-oss-20b-1:0 now use a Bedrock Converse adapter, matching the
main agent's bedrock_converse transport.
Review findings on the salvaged #60332 breaker, fixed as follow-ups:
- restore_primary_runtime() now resets the streak (third provider-swap
path; without it a recovered primary was short-circuited before a
single attempt and could never be re-proven healthy except via /model).
- interruptible_api_call (non-streaming) now carries the same breaker
(guard at entry, bump on stale_call_kill, reset on success). Quiet-mode
/ subagent / headless sessions — the profile most like #58962's
unattended 494-failure session — take this path and had the identical
infinite stale-retry class.
- Partial-stream stub return now resets the streak (chunks were received,
provider demonstrably responsive).
- Consolidated the triple-duplicated counter arithmetic into shared
helpers (_stale_streak/_bump_stale_streak/_reset_stale_streak/
_check_stale_giveup) with one canonical comment block; error message
now says 'consecutive stale attempts' (the counter counts kills, not
turns — a single turn can produce several).
4 new tests (restore resets / no-op restore keeps latch / non-streaming
short-circuit / non-streaming success reset).
Follow-up for the salvaged #60332 circuit breaker. The breaker latches:
once the streak trips, interruptible_streaming_api_call raises before any
stream is attempted, so the on-success reset can never run again. The
error text tells the user to switch models and retry — but neither
switch_model() nor try_activate_fallback() cleared the streak, so a
freshly selected healthy provider kept short-circuiting forever (only
/new recovered), and the automatic fallback chain was wedged the same way.
Reset the streak at both swap sites (after a successful rebuild only;
rollback/exhaustion paths keep the latch). 4 tests.
A session wedged against an unresponsive OpenAI-compatible provider can hit the stale-stream detector on every turn and loop forever, burning the full 180s x retries each turn with no response. Issue #58962 reports 494 consecutive failures over 3+ days on a single session.
The streaming retry path already caps retries WITHIN a turn (HERMES_STREAM_RETRIES, default 2) but has no cross-turn cap. Once a session's conversation state makes every turn stale, it retries indefinitely across turns and never notifies the user.
Add a per-session consecutive-stale-stream counter on the agent:
- incremented on every stale-stream kill in the outer poll loop;
- reset to 0 only when a stream actually completes;
- when it reaches HERMES_STREAM_STALE_GIVEUP (default 5), the next turn aborts immediately with a clear, actionable RuntimeError instead of spending 180s x retries again.
This is distinct from the existing stale-stream work (local-provider hard ceiling #44938, backoff/parse-error #60031): those bound a single hung stream, while this bounds repeated cross-turn staleness and surfaces a user-visible error.
Adds tests/run_agent/test_stream_stale_circuit_breaker.py covering the short-circuit, the success-reset, and the increment.
Same 10062 degrade-gracefully pattern as _run_simple_slash: create the
thread anyway, skip the ephemeral followups that need a live
interaction token. Non-expiry defer errors still raise.
- export now shares _add_session_filter_args / build_prune_filters with
prune/archive: AGE grammar (5h/2d/1w/ISO) on --older-than plus the full
filter set (--model, --provider, --min-messages, --min-cost, --branch,
--chat-id, ...) for both JSONL and md/qmd bulk exports; --dry-run works
on JSONL too; removes the one-off list_export_candidates helper.
- new --redact flag runs exported message content and tool output through
force-mode secret redaction (agent.redact) for jsonl, md, and qmd.
- docs EN + zh-Hans updated; new tests for AGE grammar, extended filters,
filtered JSONL, and redaction.
Replaces the parallel export-md subcommand from the salvaged commit with
a --format jsonl|md|qmd flag on the existing export subcommand, so all
session export formats share one surface. Adds AUTHOR_MAP entry.
Salvage follow-up for PR #59542 by @web3blind.
_handle_active_session_busy_message (the busy_session_handler most
platform adapters register) demotes busy_input_mode='interrupt' to
queue semantics for two reasons: active subagents (#30170) and, as of
this week, context compression in flight (#56391) — interrupting while
compression holds the state.db lock races a new turn against the
pre-rotation parent session, and if that new turn also grows past the
compression threshold it starts its own uncancellable compression on
the same stale parent, forking orphaned compression siblings.
_handle_message has its own, independent inline "PRIORITY" busy-path
(reached directly with a live running agent — see the `if _quick_key in
self._running_agents:` guard, exercised end-to-end by the existing
tests/gateway/test_running_agent_session_toggles.py harness). Its own
comment says it mirrors _handle_active_session_busy_message's subagent-
demotion rationale verbatim, and it does demote for active subagents,
but it never checked _session_has_compression_in_flight, so a plain-text
follow-up landing on this path while compression is mid-flight still
called running_agent.interrupt() unconditionally.
Fix: add the same _session_has_compression_in_flight(session_key) check
before the PRIORITY interrupt call, demoting to queue exactly like the
sibling path.
Tests: tests/gateway/test_priority_path_compression_demotion_56391.py
drives _handle_message end-to-end (reusing the test_running_agent_
session_toggles.py harness pattern) with a live running agent and a
mocked compression lock. Mutation-verified: reverting the fix makes the
demotion test fail (interrupt() gets called) against the pre-fix code;
a control test pins the unchanged default-interrupt behavior when no
compression lock is held.
npm postinstall hooks spawn cmd.exe child processes that could not resolve
`node` even when the installer found npm — causing desktop workspace npm
install to fail with exit 1 (#48130).
Extends the send_exec_approval embed-invisibility fix to its three
sibling prompt surfaces — send_slash_confirm, send_clarify, and
send_update_prompt — via a shared _self_contained_prompt_content()
helper. All four interactive views now carry their payload in plain
content next to the buttons; the embed stays as progressive
enhancement for clients that render it. Adds gold to the conftest
discord Color mock (update prompt is the only gold user).
Adds grab-to-pan horizontal scrolling to the Kanban dashboard board
columns with grab/grabbing cursor feedback. Card drag-and-drop, add
buttons, checkboxes, links, and inputs are excluded from panning, the
native horizontal scrollbar remains usable as a fallback, and panning
state is cleared on mouse release or window blur.
Salvaged from PR #59830 by @vampyren (original commit authored under an
unlinked local git identity; rewritten to their GitHub noreply address
to preserve attribution).
Discord's ExecApprovalView, SlashConfirmView, UpdatePromptView, and
ClarifyChoiceView hardcoded timeout=300, ignoring approval timeout
configuration. All four now read approvals.discord_prompt_timeout from
config.yaml (default 300s, clamped 30-900s — Discord interaction tokens
expire at ~15 min, so values beyond 900s would render dead buttons).
Surgical reapply of the timeout portion of PR #45904; the unrelated
channel-context changes bundled in that PR were intentionally excluded.
Co-authored-by: cruzanstx <cruzanstx@users.noreply.github.com>
BaseEnvironment writes shell snapshots and cwd metadata through the process
umask. With a common 022 umask, snapshot files containing exported environment
state landed at mode 0644 even though they can include env-carried credentials
from the parent process.
Set umask 077 only around Hermes metadata writes: the initial snapshot
bootstrap and the post-command snapshot/cwd refresh. User commands still run
under the caller's original umask, while Hermes-owned snapshot and cwd files
are created owner-only.
This intentionally does not copy the source PR's global orphan sweep; deleting
all matching /tmp snapshot files could interfere with concurrent Hermes
processes. The security-critical local disclosure fix is the file mode clamp.
This is salvageable because the source report still identifies a concrete
credential-disclosure path, but the safe subset is smaller than the original
proposal: clamp only the Hermes-owned snapshot writes and leave process-wide
cleanup, user command umask, and concurrent sessions alone.
Salvages source PR: https://github.com/NousResearch/hermes-agent/pull/20056
Related issue: https://github.com/NousResearch/hermes-agent/issues/48441
Co-authored-by: Andrew Homeyer <andrew@hndl.app>
_SlashWorker call sites now pass profile_home=; the fakes' 2-arg
__init__ raised TypeError inside the spawn guard, leaving
slash_worker=None and failing the orphan-race regression tests.
Same bug class as skills_tool: module-level SKILLS_DIR pinned at import
under the launch HERMES_HOME makes skill_manage() write/edit against the
wrong profile in long-lived multi-profile runtimes. Apply the same
_skills_dir() call-time resolution (honoring explicit test patches of
SKILLS_DIR) to _containing_skills_root, _resolve_skill_dir,
_find_skill_in_other_profiles, and create-result path reporting.
Refs #40677
Profile-local skills are unavailable in Dashboard/TUI/Desktop GUI because the
_SlashWorker subprocess is spawned with os.environ.copy() but does NOT receive
the profile-specific HERMES_HOME from the parent session. This causes the
subprocess to search ~/.hermes instead of the active profile's skills directory.
1. Modify _SlashWorker.__init__ to accept optional profile_home parameter
2. When profile_home is provided, set env['HERMES_HOME'] = profile_home before
spawning the subprocess
3. Update all 4 call sites to pass profile_home=session.get('profile_home')
4. Add regression tests for profile-home propagation
- Full TUI gateway test suite: 107 tests pass
- New tests cover:
- profile_home parameter acceptance
- backward compatibility (None, omitted)
- argv correctness
Fixes#40677
Discord thread names share the same UTF-16 component budget as select
labels and buttons — route the sanitizers in gateway/run.py and the
adapter's rename_thread through utf16_len/_prefix_within_utf16_limit
instead of code-point slices. Adds rungmc357 to AUTHOR_MAP.
Sessions no longer auto-reset by default. SessionResetPolicy.mode now
defaults to "none" (was "both": 24h idle + daily 4am), matching the
setup wizard's existing no-reset default and community feedback that
surprise context loss hurts more than it helps.
- gateway/config.py: dataclass default + from_dict fallback -> "none";
installs whose config.yaml lacks a session_reset section stop
auto-resetting
- hermes_cli/setup.py: "Never auto-reset" is now the recommended/default
choice in hermes setup agent; stale comment updated
- docs (en + zh-Hans): default is no auto-reset, opt in via
session_reset in config.yaml
Users who explicitly configured idle/daily/both resets keep them.
/new's handler calls _reset_notice_session_info, which resolves live
provider credentials and can probe model context length over HTTP. In
CI there are no credentials, so resolution walks the entire fallback
chain (the failed run's log shows 'Primary provider auth failed ...
trying fallback' captured inside the test) and on a slow runner the
first parametrization can blow past send_and_capture's 2s poll window,
making adapter.send appear never-called.
Stub it to return an empty info block in the e2e runner fixture — these
tests exercise gateway command dispatch, not provider resolution, and no
other network-touching path exists in the /new flow. Flaked in run
28856659216 (telegram param only); tests/e2e now 57/57 locally.
Replace the hand-rolled ensurepip bootstrap (and five other one-off
pip-install code paths) with hermes_cli.tools_config._pip_install, which
prefers the bundled uv (fast, needs no pip in the venv), falls back to
python -m pip, and bootstraps pip via ensurepip only when missing.
Sites unified:
- hermes_cli/setup.py: _install_neutts_deps, _install_kittentts_deps,
modal SDK install, daytona SDK install
- hermes_cli/memory_setup.py: memory-plugin pip deps (previously dead-ended
when uv AND pip binaries were both absent)
- hermes_cli/dingtalk_auth.py: qrcode auto-install (previously invoked
'python -m uv' which is not how uv ships)
- agent/lsp/install.py: --target LSP server installs
- plugins/google_meet/cli.py, plugins/platforms/matrix/adapter.py,
plugins/platforms/google_chat/oauth.py, plugins/memory/honcho/cli.py
Tests updated to assert the ladder behavior (uv-first, pip fallback,
ensurepip bootstrap) instead of the removed bespoke branches.
Follow-up on the salvaged fix, which bounded start_polling() only in
_handle_polling_network_error. The same wedge (#59614) exists at the two
sibling call sites:
1. _start_polling_resilient (bootstrap): an exhausted pool hangs connect()
forever. The TimeoutError from wait_for is a builtins TimeoutError
(OSError subclass), so the existing except classifies it via
_looks_like_network_error and schedules background recovery.
2. _handle_polling_conflict (conflict-retry ladder): identical hang wedges
conflict attempt N forever; timeout now converts to RuntimeError and the
existing except schedules the next attempt.
Tests replaced with a stronger suite: hung-network-ladder repro (RED without
the fix), bootstrap hang schedules recovery, success-path sanity, and a
bug-class contract test asserting EVERY updater.start_polling( call site is
wrapped in wait_for so a new unbounded site can't reintroduce the wedge.
Verified RED (3 failures) with the wrappers removed, GREEN with them.
When the connection pool is in a degraded state after
_drain_polling_connections(), start_polling() can hang indefinitely
when both primary and fallback Telegram endpoints are unreachable. The
httpx client may hold a stale socket that neither connects nor times out
within PTB's internal flow, causing the reconnect ladder to stall at
attempt 1/10 forever.
Wrap start_polling() in asyncio.wait_for() with a 30-second timeout so a
hung call raises asyncio.TimeoutError and feeds back into the existing
retry ladder. This unblocks:
- The 10-retry ladder advances to attempt 2, 3, ...
- The heartbeat loop sees _polling_error_task.done() and can trigger recovery
- The reconnect watcher gets the adapter in _failed_platforms
Fixes#59614
Port from anomalyco/opencode#31877: JSON Schema type arrays like
["number","string"] (common in MCP tool schemas) were collapsed to the
first non-null type, silently dropping every other branch. Several
tool-call backends reject the array form outright — llama.cpp's grammar
generator and Gemini via OpenAI-compatible transports (e.g. GitHub
Copilot proxying to Gemini) 400 on it.
_sanitize_node now mirrors @ai-sdk/google: a single non-null type stays
type:X (+nullable if null was present), multiple non-null types become
an anyOf of single-type schemas so no branch is lost, and an all-null
array becomes type:null. Single-null collapse is unchanged.
Verified nested (object props, array items) survive the full sanitize
pipeline — combinator stripping is top-level-only and nullable-union
collapse only fires on single-survivor unions, so multi-type anyOf is
left intact.
Replace the unittest.mock module-name check with an
inspect.iscoroutinefunction probe on content.read, and collapse the
duplicate read/iter_chunked reader paths into one. Non-streaming
objects (test doubles, proxy wrappers) fall back to the response's
native json()/text() as before.