Commit graph

1255 commits

Author SHA1 Message Date
kshitijk4poor
4ca61869cb docs: sharpen bypass comment per review
The live path bypasses the whole _build_gateway_agent_history cleanup
pipeline, not just the replay-cleanup pass — say so precisely.
2026-07-08 00:59:56 +05:30
pierrenode
d33becd877 fix(gateway): demote PRIORITY-path interrupt to queue during compression (#56391)
_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.
2026-07-07 21:06:07 +05:30
teknium1
1deeaf71ab fix(discord): truncate thread titles by UTF-16 units + AUTHOR_MAP
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.
2026-07-07 05:11:59 -07:00
Georgio Constantinou
0d9ed9214d Add semantic titles for Discord auto-threads 2026-07-07 05:11:59 -07:00
kshitij
7647eff360
Merge pull request #60117 from kshitijk4poor/fix/59607-cached-agent-expiry
fix(gateway): re-apply confirmation expiry on the cached-agent live-history path (#59607)
2026-07-07 15:14:24 +05:30
teknium1
87b65e24a7 refactor(compression): scope Codex-native compaction to the app-server runtime
Drop the Responses-API native compaction path and its opt-in umbrella
flag from the salvaged feature. On the Codex OAuth chat route Hermes
owns the message list and the summary compressor works (and stays
provider-portable — encrypted compaction items would lock the session
history to chatgpt.com and break /model switches and provider
fallback). On the app-server runtime (codex CLI/agent) the codex agent
owns the real thread context, so thread/compact/start is the only
mechanism that can actually shrink it (#36801) — that path is now the
default behavior for codex_app_server sessions, controlled by
compression.codex_app_server_auto (native|hermes|off), no umbrella
flag.

Removed: responses.compact() call path, codex_compaction_items replay/
persistence plumbing, codex_native_compaction + codex_responses_threshold
config keys, desktop settings fields, and their tests. Kept: everything
app-server (compact_thread(), compaction notifications, bookkeeping,
docs, tests) plus cache-busting keys for the surviving knobs.
2026-07-07 02:39:54 -07:00
kshitijk4poor
3c8130a826 fix: re-apply confirmation expiry on the cached-agent live-history path
Review finding: when the FTS write-corruption guard (#50502) prefers the
cached agent's live _session_messages over the reloaded transcript, that
history bypasses the replay-cleanup pass in _build_gateway_agent_history
— a stale dangerous confirmation could slip through unredacted on the
same-process salvage path. Re-apply the (idempotent) expiry stripper to
the selected live history.
2026-07-07 14:46:10 +05:30
knoal
33a529538d fix(gateway): strip stale dangerous-confirmation text in user messages (#59607)
When a high-risk side effect (e.g. host restart via shutdown.exe) runs,
the user's plain-text confirmation phrase is persisted in the conversation
transcript. If the host restart killed the gateway process before the
assistant's tool result was written, the transcript tail ends on the
assistant's text response - and the dangerous confirmation text remains
in the user role.

On the next inbound message - possibly a casual 'are you there?' from
the user minutes later - the LLM sees the stale confirmation and may
interpret the new turn as a fresh re-confirmation, re-executing the
destructive action. This is the failure mode reported in #59607.

Fix:
- Add strip_stale_dangerous_confirmations() in agent/replay_cleanup.py
  that removes user messages whose content matches a known dangerous
  confirmation pattern AND whose timestamp is older than 60 seconds.
- Add is_dangerous_confirmation() helper with the matched patterns
  (i18n-aware: covers 確認強制重開機 from the original incident).
- Wire the stripper into _build_gateway_agent_history() right after the
  existing 75ed07ace strippers, so the strip chain is:
  strip_interrupted_tool_tails -> strip_dangling_tool_call_tail ->
  strip_stale_dangerous_confirmations.
- Update _build_replay_entry() to preserve the timestamp on user
  messages (it was previously dropped), since the new stripper needs it.

Complements 75ed07ace (which strips the assistant side of the broken
tail) by handling the user side: a stale plain-text confirmation that
the assistant has not yet responded to in a way the resume logic
recognises.

Failing-test-first discipline: the bug-detection test
test_stale_confirmation_text_is_stripped_on_resume fails on unfixed
code (proves the test catches the bug) and passes after the fix.
Five additional safety tests confirm no regression on:
- fresh confirmations (within expiry) are preserved
- non-confirmation text is preserved
- non-matching histories are untouched
- dangerous-pattern detection works in all cases (case, i18n, None)
- direct unit test of the strip helper

Refs: #59607
2026-07-07 14:40:32 +05:30
Teknium
76979a0869
fix(auth): per-profile Anthropic OAuth file + complete port-binding platform set (#57563 salvage) (#59339)
* fix(auth): resolve Anthropic OAuth file per-profile + close port-binding platform gaps

Two focused pieces salvaged from PR #57563:

1. _HERMES_OAUTH_FILE was computed at module import time — frozen before
   HERMES_HOME/profile overrides, so multiplexed profile turns read and
   wrote the DEFAULT profile's .anthropic_oauth.json (OAuth path hijack).
   Replaced with a lazy _get_hermes_oauth_file(); all web_server.py call
   sites updated.

2. _PORT_BINDING_PLATFORM_VALUES was missing whatsapp_cloud and line —
   both bind aiohttp TCP listeners, so a secondary multiplex profile
   enabling them would collide with the primary's listener instead of
   failing fast at startup.

Original work by @austinlaw076. The rest of #57563 was redundant on
main (adapter routing sweep superseded by #56854's salvage; cron secret
scope landed in fdab380a1; nested-config fallback in from_dict).

* chore(release): map austinlaw076 author email for PR #57563 salvage

* test(hermes_cli): patch _get_hermes_oauth_file instead of removed _HERMES_OAUTH_FILE constant

---------

Co-authored-by: Austin <austin@openvm067.space>
Co-authored-by: Ben <ben@nousresearch.com>
2026-07-07 08:33:06 +00:00
Teknium
249c69b958
fix(gateway): per-profile pairing whitelist isolation in multiplex mode (#53045 salvage) (#59330)
* fix(gateway): per-profile pairing whitelist isolation for multiplex gateways

Pairing approvals are stored per profile (profiles/<name>/pairing/) and
authz routes pairing checks through the serving profile's store, so one
profile's approved users no longer authorize against every other
profile's whitelist in multiplex mode.

The global store remains for the hermes pairing CLI and single-profile
gateways; unregistered/unstamped sources fall back to it, preserving
existing behavior.

Salvaged from PR #53045 (pairing half). The SOUL.md half was dropped:
the agent turn already runs inside _profile_runtime_scope on main, so
load_soul_md() resolves per-profile without changes.

Original work by @soddy022.

* ci: redispatch after arm64 docker dashboard-slot flake (unrelated to this PR)

---------

Co-authored-by: soddy022 <290613374+soddy022@users.noreply.github.com>
2026-07-07 18:25:52 +10:00
Teknium
088b989442
fix(gateway): scope reset banners' session info to the serving profile (#59048 salvage) (#59329)
* fix(gateway): scope reset banners' session info to the serving profile

The auto-reset notice and the manual /reset //new banner both appended
_format_session_info() outside any profile scope, so a multiplexed
gateway advertised the base config's model/provider/context while the
session actually ran on the profile's.

Route both call sites through a new _reset_notice_session_info(source),
which enters _profile_runtime_scope for the source's profile when
gateway.multiplex_profiles is on (mirroring _run_agent's gating), so
_load_gateway_config()/_resolve_gateway_model() resolve the profile's
config.yaml via the existing context-local home override. Single-profile
gateways never enter the scope — behavior unchanged.

Both call sites invoke the helper via asyncio.to_thread: under the
scope, resolution can do blocking work (credential refresh,
context-length HTTP probes) that previously failed fast unscoped and
must not run on the event loop.

Fixes #59003

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* chore(release): map irresi author email for PR #59048 salvage

---------

Co-authored-by: irresi <blueirobin02@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 18:25:45 +10:00
Teknium
d297568299
fix(gateway): detect config token credential collisions (#59321)
Co-authored-by: markoub <2418548+markoub@users.noreply.github.com>
2026-07-07 18:25:33 +10:00
Waseem Shahwan
777cfa81f3 fix(gateway): drop interrupt sentinel before chat delivery (#7921) 2026-07-06 03:53:52 -07:00
AIalliAI
a14caf7759 fix(gateway): stop post-/stop stale interrupt from silently swallowing the next message
A /stop sets _interrupt_requested on the session's cached agent, but the
flag is only cleared by the turn finalizer.  When the stopped run is hung
or still draining, the flag survives the forced lock release and the
session's NEXT user message is killed at the top of the tool loop
(conversation_loop.py interrupt check): the run completes with
interrupted=True, api_calls=0 and an empty response, which
_normalize_empty_agent_response passed through as pure silence — the
user's message was swallowed with no trace except a
'response ready: ... api_calls=0 response=0 chars' log line.

Two-layer fix:

- _interrupt_and_clear_session now evicts the cached agent whenever it
  releases the running state.  The next message rebuilds the agent from
  session history (mirroring the /new and /model paths), while the old
  agent object keeps its interrupt flag so a hung drain still dies when
  it unblocks.  This intentionally does NOT clear the flag in place:
  turn_context deliberately preserves a pending interrupt across turn
  start (it carries interrupt-message delivery), and clearing it could
  revive a hung run the user just stopped.

- _normalize_empty_agent_response distinguishes a drain from a swallowed
  turn: an interrupted run that did work (api_calls > 0) stays silent as
  before (deliberate stop/steer; queued messages are delivered by the
  recursive drain inside _run_agent), but an interrupted run with ZERO
  api_calls never processed the user's message at all and now surfaces a
  'send it again' notice instead of nothing.

Same silent-delivery class as a1f76ba7e (#29346), which covered the
extract-stripped case; regression tests added next to that coverage.

Fixes #44212
2026-07-06 03:42:14 -07:00
teknium1
ab70551b3d fix(gateway): fail-closed adapter resolution for unregistered secondary profiles
Follow-up to the routing sweep: when a stamped secondary profile has no
_profile_adapters entry (adapter failed to connect / was refused), return
None instead of falling back to the default profile's adapter — the
fallback sends replies out the wrong bot, which is the exact leak class
this cluster fixes. Also restores main's deliberate fail-fast on
port-binding platforms in secondary profiles (the cherry-picked commit
had softened it to silent force-disable).

Co-authored-by: ManniBr <m888.braun@hotmail.com>
2026-07-05 21:48:59 -07:00
Andreas Hiltner
8a9bc38c2e fix(gateway): route multiplex profile responses through correct adapter
Replace 53 instances of self.adapters.get(source.platform) with
self._adapter_for_source(source) in gateway/run.py.

self.adapters is the default profile's adapter map. In multiplex mode,
secondary profiles (lars, kira, jonas, caro) have their adapters in
_profile_adapters[profile]. _adapter_for_source() (from authz_mixin.py)
correctly resolves through _profile_adapters when source.profile is set.

Without this fix, ALL response paths for secondary profiles — streaming,
sending, media delivery, voice, typing indicators, queue operations,
startup restore, and platform notices — route through the default
profile's bot token instead of the profile's own token.

Fixes: Multiplex profiles responding with wrong bot token on Telegram,
Discord, and all other platforms.
2026-07-05 21:48:59 -07:00
Teknium
65117671e3
fix(gateway): apply platform-disabled skill gate to bundle invocations (#59156)
Skill bundles load their member skills via _load_skill_payload directly,
bypassing the scan-time disabled filter in get_skill_commands(). PR #58888
closed this gap for stacked slash-skill invocations, but /<bundle> dispatch
in the gateway had the same class of bypass: a skill an operator disabled
for a platform via skills.platform_disabled still got its full content
injected when referenced by a bundle.

build_bundle_invocation_message() now accepts a platform kwarg, filters
members against get_disabled_skill_names(platform=...), and reports skipped
skills in the bundle header. Gateway dispatch passes the event's platform
explicitly (env-var resolution can't be trusted in the multi-platform
gateway process, same reasoning as the #58888 gate).
2026-07-05 14:48:11 -07:00
Teknium
cd2b360d64 feat: add Docker terminal network toggle
Port from qwibitai/nanoclaw#2713: expose Hermes' existing Docker network isolation primitive through terminal config so operators can opt out of container egress.
2026-07-05 14:41:51 -07:00
Teknium
747386ecfa
refactor: consolidate gateway session metadata into state.db (#58899)
Moves gateway routing metadata (display_name, origin_json, expiry_finalized)
into state.db, making SQLite the single source of truth for gateway session
discovery. Eliminates the dual-file (sessions.json + state.db) polling
dependency that caused the mcp_serve new-conversation race (#8925).

- hermes_state.py: schema v18 (3 new sessions columns + sessions.json
  backfill migration), record_gateway_session_peer gains
  display_name/origin_json, new set_expiry_finalized(),
  list_gateway_sessions(), find_session_by_origin()
- gateway/session.py: peer recorder persists display_name + full origin
  JSON; new SessionStore.set_expiry_finalized() single write-path
- gateway/run.py: expiry watcher success + give-up paths use the store
  helper so the flag lands in both sessions.json and state.db
- mcp_serve.py: routing index reads state.db first (sessions.json fallback
  for pre-migration DBs); _poll_once collapses to a single state.db mtime
  check — the #8925 race is structurally impossible now
- gateway/mirror.py, gateway/channel_directory.py, hermes_cli/status.py:
  query state.db first, sessions.json fallback

Closes #9006
2026-07-05 14:01:03 -07:00
srojk34
cdcbc3a31d fix(gateway): clear last-resolved-model cache on 3 more conversation-boundary resets
11b4a21a5 cleared the per-session _last_resolved_model cache on /new and
the compression-exhausted auto-reset, so a resumed/reset conversation
resolves the model from current config instead of a stale cached value
(#58403). Three other sites documented as the same "full conversation
boundary" treatment — pop _session_model_overrides, clear the reasoning
override, pop _pending_model_notes — still missed _last_resolved_model:

- _session_expiry_watcher's permanent finalization block (gateway/run.py):
  a session that goes idle and is finalized, then resumed, could serve a
  model cached before it went idle on a transient config-cache miss.
- The daily/idle/suspended auto-reset cleanup (_was_auto_reset handling,
  gateway/run.py): same failure mode, different trigger.
- /resume (gateway/slash_commands.py), whose own comment already says
  "conversation boundary just like /new" for the sibling dicts it clears.

Fix: pop the session's _last_resolved_model entry in all three, mirroring
the exact pattern 11b4a21a5 established.
2026-07-05 13:58:03 -07:00
srojk34
04d732dc56 fix(gateway): re-check every stacked skill against the platform-disabled list
_handle_message() re-checks a slash-skill command's per-platform disabled
status before dispatch, because get_skill_commands() only applies the
global disabled list at scan time. That check only covered the leading
skill: split_stacked_skill_commands() resolves additional /skill tokens
that follow it (stacked invocations, up to 5 skills, #57987), and
build_stacked_skill_invocation_message() loads every one of them via
_load_skill_payload() with no disabled-status check of any kind.

A message on a platform with skills.platform_disabled configured for a
given skill could still get that skill's full SKILL.md content injected
into the agent's context for the turn, as long as it was typed after an
allowed skill: `/allowed-skill /disabled-skill do X`.

Fix: after computing the stacked extra_keys, look up each one's skill
name and re-check it against the same get_disabled_skill_names(platform=)
set already used for the leading skill. If any stacked skill is disabled
for the platform, reject the whole invocation with the same style of
message the leading-skill check already returns, instead of partially
loading it.
2026-07-05 13:57:11 -07:00
kshitijk4poor
18058c4515 fix(gateway): drain housekeeping thread over its own 30s future on shutdown
Follow-up on the #58818 cron-drain fix. The housekeeping ticker uses the
same loop-scheduled-future pattern as cron — it refreshes the channel
directory via safe_schedule_threadsafe(build_channel_directory(...), loop)
and blocks on fut.result(timeout=30). The original fix swapped its
join(5) for _await_thread_exit(5), which is a strict improvement (the loop
stays alive so the future can run) but the 5s bound is shorter than the
30s future, so a refresh in flight at shutdown was still abandoned. Bound
the housekeeping drain at 35s (30s future + margin) via a dedicated
_HOUSEKEEPING_SHUTDOWN_DRAIN_TIMEOUT constant. Not user-facing (self-heals
next tick) but keeps the cooperative drain honest across both threads.
2026-07-06 01:37:10 +05:30
HexLab98
dcd70c5823 fix(gateway): drain in-flight cron delivery on restart instead of dropping it
A cron delivery uses the live adapter by scheduling the send coroutine onto the
gateway event loop (safe_schedule_threadsafe) and blocking the ticker thread on
future.result(). On shutdown/restart the cleanup ran a synchronous
cron_thread.join(timeout=5), which blocks the event loop — so the pending
delivery coroutine could never execute, the join always timed out, and the
message was silently dropped (#58818). The default agent.restart_drain_timeout
is 0, so this fired on every restart with an in-flight delivery.

Replace the blocking joins with _await_thread_exit(), which polls is_alive()
via await asyncio.sleep so the loop keeps running and finishes the queued
delivery before teardown. The cron wait is bounded by the delivery future's own
60s ceiling (plus margin); housekeeping keeps a short bound. When no delivery is
in flight the ticker exits on stop_event immediately, so shutdown stays snappy.
2026-07-06 01:37:10 +05:30
devatnull
d111faa3a7 fix: preserve busy steer env override 2026-07-05 06:29:26 -07:00
devatnull
12f03b11ff feat: make busy steer ack configurable 2026-07-05 06:29:26 -07:00
devatnull
fddc95f4c2 chore: limit generic status phrases to long-running notifications 2026-07-05 06:29:26 -07:00
devatnull
4bf5b563bd feat: add generic gateway status phrases 2026-07-05 06:29:26 -07:00
devatnull
406eb719c3 fix: gate interrupt STT transcript echoes 2026-07-05 06:12:49 -07:00
devatnull
bfc5262725 feat: add STT transcript echo toggle 2026-07-05 06:12:49 -07:00
Teknium
9767e19b60
feat(skills): stacked slash-skill invocations — /skill-a /skill-b do XYZ (#57987)
Inspired by Claude Code v2.1.199 (July 2, 2026): stacked slash-skill
invocations load all leading skills (up to 5), not just the first.

- agent/skill_commands.py: split_stacked_skill_commands() consumes leading
  /skill tokens (stops at the first non-skill token so slash-path arguments
  are never swallowed); build_stacked_skill_invocation_message() composes
  the multi-skill turn reusing the existing bundle scaffolding markers so
  extract_user_instruction_from_skill_message() keeps memory providers
  storing the user's instruction, not N skill bodies.
- cli.py + gateway/run.py: dispatch the stacked path on both surfaces.
- 11 new tests + docs section in skills.md.
2026-07-05 02:20:01 -07:00
Teknium
485ae54c9f
fix(gateway): pass full transcript to compressor instead of filtered messages (#58551)
Both gateway compression entry points (session-hygiene auto-compress in
run.py; manual /compress in slash_commands.py) filtered the transcript
to user/assistant-only, content-bearing messages before calling
_compress_context. That starved the compressor:

- tool results are usually the bulk of the context, and
  _prune_old_tool_results never saw them
- short filtered histories tripped the protect-first/last early-return,
  so compression became a no-op even on huge sessions
- assistant tool_calls stubs (content=None) were dropped, so even the
  summary lost the tool activity

Pass user/assistant/tool messages through intact, matching what the
agent loop itself feeds _compress_context.

Port of PR #3854 onto current main (the manual-compress handler moved
from run.py to slash_commands.py since the PR branched); regression test
asserts tool messages reach the compressor.

Authored-by: David Zhang <david.d.zhang@gmail.com> (@Git-on-my-level)

Co-authored-by: David Zhang <david.d.zhang@gmail.com>
2026-07-05 00:43:48 -07:00
luyifan
5b8593266f fix(gateway): cap proxy SSE line buffer 2026-07-05 00:41:34 -07:00
kshitijk4poor
1388cd1c0c fix(logging): thread-safe queue state + bounded hard-exit drain + record copy
Self-review (3-agent + codex) findings on the async QueueListener change:

1. (HIGH) The os._exit shutdown backstop called flush_log_queue(), whose
   stop() joins the listener thread unbounded. If that thread is wedged on
   the rotation lock — the exact failure this change survives — shutdown
   re-freezes. Add drain_log_queue(timeout): stop-only, bounded via a
   throwaway joiner thread. Also release PID/runtime locks BEFORE the drain
   so a slow drain can't strand them.

2. (MED) _log_queue/_queue_listener/_queued_file_handlers were read-modify-
   written without a lock across register/stop/flush/reset; a gateway-init
   race with a plugin/CLI path could leave two live listeners. Guard all
   four globals with a single _queue_state_lock.

3. (MED) _NonFormattingQueueHandler.prepare() enqueued the same LogRecord a
   synchronous handler on the emitting thread may still format/mutate.
   Return copy.copy(record) (preserves msg/args/exc_info for deferred
   RedactingFormatter) to remove the cross-thread mutation race.

E2E-verified: bounded drain returns in ~500ms on a permanently-wedged
listener; 4x20 concurrent flushes single-listener no-crash; args still
format and secrets still redact through the copied record.
2026-07-05 11:44:53 +05:30
kshitijk4poor
ac68a6411a fix(gateway): drain async log queue on os._exit shutdown backstop
The QueueListener change routes rotating file handlers through an
in-memory queue drained on a dedicated thread, with an atexit hook to
flush on shutdown. But _exit_after_graceful_shutdown() uses os._exit,
which bypasses atexit — so on the early-exit and #53107 hard-exit paths
the queued records (including the shutdown reason) were silently lost.

Explicitly flush_log_queue() before os._exit, and correct the now-stale
comment that claimed handlers are synchronous with nothing pending.
2026-07-05 11:44:53 +05:30
yoma
7e8f50a141 fix(gateway): load display config from routed profile 2026-07-04 16:34:16 -07:00
liuhao1024
11b4a21a56 fix(gateway): clear last-resolved-model cache on /new and compression auto-reset
After a config change (e.g. switching model provider), the /new command
must clear the per-session _last_resolved_model cache so the next turn
resolves the model from the updated config instead of falling back to
the stale cached value.

Without this fix, if a transient config-cache miss occurs on the first
post-/new turn, the #35314 recovery path serves the old model from the
cache — the user sees the old model being used even though they changed
config.yaml and explicitly ran /new.

Fix applies to both call sites that reset session model state:
- GatewaySlashCommandsMixin._handle_reset_command (slash_commands.py)
- GatewayRunner compression-exhausted auto-reset (run.py)

Fixes #58403
2026-07-04 16:34:16 -07:00
Tuna Dev
ff4c8172ce fix(gateway): attach credential_pool to session /model overrides
Per-session /model overrides supplied api_key and provider but omitted
credential_pool, so billing rotation never ran on HTTP 402. Wire the pool
on fast override, rehydrate, and apply paths; backfill from provider for
legacy persisted overrides. Regression tests in tests/gateway/.
2026-07-04 16:34:16 -07:00
teknium1
c6dc7c03c3
Revert "Merge pull request #30179 from NousResearch/feat/iron-proxy"
This reverts commit 8790adc4c6, reversing
changes made to fe5054bccf.
2026-07-04 13:38:59 -07:00
Teknium
8790adc4c6
Merge pull request #30179 from NousResearch/feat/iron-proxy
feat(egress): iron-proxy credential-injection firewall for sandboxes
2026-07-04 13:29:23 -07:00
infinitycrew39
1cc68fc897 fix(gateway): resolve terminal.cwd placeholders per backend and mount mode
Split placeholder TERMINAL_CWD resolution into three cases: local falls
back to MESSAGING_CWD/home; docker without workspace mount leaves cwd unset;
docker with mount enabled preserves an explicit host MESSAGING_CWD path for
terminal_tool's /workspace mapping. Stops leaking host Path.home() into
containers without breaking the mount contract.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-04 13:28:47 -07:00
teknium1
14cbbd541e
Merge remote-tracking branch 'origin/main' into iron-proxy-followups
# Conflicts:
#	hermes_cli/config.py
#	hermes_cli/main.py
#	website/docs/reference/cli-commands.md
2026-07-04 03:09:43 -07:00
kshitijk4poor
4651ac64a1 refactor(ssh): extract shared _is_ssh_remote_tilde_cwd predicate
Follow-up to the salvaged SSH-tilde-cwd fix. The predicate
"backend == ssh and (cwd == ~ or cwd.startswith(~/))" was inlined at
each expanduser guard site, which is how the test simulator drifted from
production (it grew an SSH guard on a top-level-alias branch that has no
production counterpart).

- Add tools/terminal_tool._is_ssh_remote_tilde_cwd(backend, cwd) as the
  single source of truth (case/whitespace-tolerant).
- Use it in _get_env_config and the gateway config bridge.
- Test simulator imports the real helper instead of re-implementing the
  predicate; revert the phantom SSH guard on the top-level-alias branch
  (production maps top-level cwd: to a plain env var, not TERMINAL_CWD via
  an SSH-guarded path — that branch tested nothing real).
2026-07-04 14:17:11 +05:30
helix4u
83fb8ec277 fix(ssh): preserve remote tilde cwd 2026-07-04 14:17:11 +05:30
Maxim Esipov
769469a703 fix: route gateway images by session model override
(cherry picked from commit 7702071c01)
2026-07-03 03:33:06 -07:00
teknium1
741bd9ba42 fix(gateway): resolve queued follow-up session key before native-image buffering
The cherry-picked #48919 fix resolved next_session_key AFTER
_prepare_inbound_message_text had already buffered native image paths
under the stale key. Reorder so the write key and the consume key are
the same resolved key.
2026-07-03 03:21:09 -07:00
LeonSGP43
bb24ac6f20 fix(gateway): preserve queued native image attachments 2026-07-03 03:21:09 -07:00
tt-a1i
e880396488 fix(gateway): key native image handoff by session 2026-07-03 03:21:09 -07:00
kshitijk4poor
201b646d67 fix(gateway): complete on_session_end coverage across all eviction paths
Follow-up to the cherry-picked #31856 fix. The contributor's guard defers
idle-TTL eviction until the session store reports the session expired, so the
expiry watcher can tear the agent down and fire MemoryProvider.on_session_end()
with the live transcript. Two gaps remained:

1. Memory-leak regression for mode='none' sessions. _is_session_expired()
   returns False forever for the 'none' reset policy, so the naive guard would
   never idle-evict those agents — reopening the unbounded-cache leak the idle
   sweep (#11565) exists to relieve. Added SessionStore.is_session_finalizable()
   (a public predicate: will the expiry watcher EVER finalize this session?) and
   gate the deferral on it. mode='none' agents fall through to soft eviction as
   before.

2. on_session_end still dropped on the LRU-cap path. Both cache-pressure paths
   (_enforce_agent_cache_cap and _sweep_idle_cached_agents) soft-evict via
   _release_evicted_agent_soft, which by design does NOT fire on_session_end.
   If cache pressure evicts a finalizable-but-not-yet-expired agent before it
   expires, the watcher later finds no cached agent and the hook is skipped.
   Added _commit_memory_before_soft_evict(): at LRU eviction, if the session is
   finalizable and not yet expired, commit end-of-session extraction via the
   live agent's own (fully-scoped) memory manager using commit_memory_session()
   — extraction WITHOUT provider teardown, so the eviction stays soft and a
   resumed turn keeps working. Skipped for mode='none' (no missed boundary to
   compensate) and expired sessions (the watcher tears those down directly).

This closes #11205 for ALL eviction paths and reset policies, not just the
idle-sweep + finite-policy case, while preserving the soft-eviction
resumability contract (never calls close() on a live session).

Tests: 5 new cases in test_agent_cache.py (mode='none' still reaped, LRU-cap
commits for finalizable / skips for none, real is_session_finalizable
predicate); all mutation-checked. Contributor's original 2 tests updated to
assert the finalizable path explicitly.
2026-07-03 03:46:43 +05:30
Hermes Trismegistus
90b618f48a fix(gateway): keep idle cached agents alive until session actually expires
The idle-TTL sweep (_sweep_idle_cached_agents) was evicting agents
as soon as they passed _AGENT_CACHE_IDLE_TTL_SECS, even when the
session hadn't expired yet. In daily-reset mode the reset can fire
hours after the last user message — evicting the agent early means
the session-expiry watcher has no agent in cache to call
on_session_end() with, so memory providers miss the live transcript.

Now the sweep checks the session store before evicting: if the
session still exists and hasn't expired, the agent stays in cache
so the expiry watcher can tear it down properly later.
When the session store is unavailable or throws, falls back to the
original eviction behavior (safe default).

Fixes: #11205
2026-07-03 03:46:43 +05:30
Brooklyn Nicholson
5a6720b884 fix(desktop,tui-gateway,zai): stop thinking-off from reverting to medium
A Z.ai desktop user reported thinking reverting to medium after one turn,
burning ~200% of a week's credits in 4 days despite reasoning_effort: false
in config.yaml. Four compounding bugs:

- _session_info reported reasoning_effort "" for disabled reasoning,
  indistinguishable from unset — the desktop adopted it after the first
  turn, wiping its sticky "thinking off" pick so every later chat
  reverted to the default effort.
- config.set key=reasoning always wrote agent.reasoning_effort to global
  config.yaml, so every desktop model-menu selection (preset.effort ??
  'medium') clobbered the user's configured value. Now session-scoped
  like the messaging gateway's /reasoning, landing on
  create_reasoning_override so lazily-built sessions keep it too.
- YAML `reasoning_effort: false`/`off`/`no` (boolean False) was coerced
  to "" by every loader's `str(x or "")`, silently re-enabling thinking.
  parse_reasoning_effort now treats False/"false"/"disabled" as
  {"enabled": False}; loaders (tui gateway, gateway, cli, cron,
  delegate) pass the raw value through. The desktop config reader also
  crashed on the boolean (false.trim()), aborting voice/STT settings.
- The zai provider profile never sent thinking on the wire, and GLM-4.5+
  defaults to thinking ON server-side — so disabling reasoning was a
  silent no-op on direct Z.ai, the actual token burner. The profile now
  emits extra_body.thinking {"type": "enabled"|"disabled"} for
  thinking-capable GLM models, mirroring the DeepSeek profile.

Also: /new (session reset) now carries reasoning_config across the
rebuild like model_override; config.get reasoning prefers the session's
live value and maps a config False to "none"; Settings shows "Off"
instead of a blank select for hand-written false.
2026-07-02 15:23:47 -05:00