Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
163 commits
Select commit Hold shift + click to select a range
d548908
fix(cron): make live-adapter delivery confirmation reliable (#38922, …
iamlukethedev Jun 21, 2026
d1cee7c
feat(gemini): present authentic @google/gemini-cli identity to Google…
arminanton Jun 21, 2026
65d7c7f
fix(cron): execute job immediately on action='run'
kyssta-exe Jun 21, 2026
02a3288
Merge pull request #50018 from NousResearch/salvage/f3a-delivery-confirm
kshitijk4poor Jun 21, 2026
f1f36b3
fix(cron): repair migrated cron timezone offsets to prevent double-fire
Tranquil-Flow Jun 21, 2026
4cc28aa
fix(cron): route Telegram DM-topic cron delivery through DeliveryRout…
kshitijk4poor Jun 21, 2026
f43c616
chore(release): add devsart95 to AUTHOR_MAP
kshitijk4poor Jun 21, 2026
3051a16
Merge pull request #50023 from NousResearch/salvage/f3b-telegram-dmtopic
kshitijk4poor Jun 21, 2026
f6a504d
Merge pull request #50025 from NousResearch/salvage/cron-run-immediate
kshitijk4poor Jun 21, 2026
f57ff7a
Merge pull request #50034 from NousResearch/salvage/cron-tz-offset-re…
kshitijk4poor Jun 21, 2026
8666fd7
fix(desktop): preserve other providers' hide-all in model visibility …
Jun 16, 2026
461fcc0
test(desktop): harden model-visibility toggle + dedupe default expansion
kshitijk4poor Jun 21, 2026
8ca38d3
Merge pull request #50100 from kshitijk4poor/salvage/model-visibility…
kshitijk4poor Jun 21, 2026
472c068
fix(mcp): detect 'unknown method' phrasing in ping keepalive fallback
xxxigm Jun 21, 2026
7b9a0b3
test(mcp): cover 'unknown method' ping keepalive fallback (#50028)
xxxigm Jun 21, 2026
04730f3
fix(cli): warn when in-session model switch will preflight-compress
tuancookiez-hub Jun 20, 2026
1ca2972
fix(cli): log instead of swallow preflight-warning errors; consistent…
kshitijk4poor Jun 21, 2026
44d552e
Merge pull request #50115 from NousResearch/salvage/model-switch-pref…
kshitijk4poor Jun 21, 2026
51a338a
feat(gateway): track active_agents in runtime status on turn boundaries
benbarclay Jun 21, 2026
0ee7546
feat(dashboard): surface gateway busy/drainable on /api/status
benbarclay Jun 21, 2026
b577f25
refactor(gateway): dedupe drain-timeout resolution + share active_age…
kshitijk4poor Jun 21, 2026
4d7bb38
refactor(gateway): route all active_agents coercion through parse_act…
kshitijk4poor Jun 21, 2026
5aec00f
Merge pull request #50131 from kshitijk4poor/salvage/gateway-busy-rea…
kshitijk4poor Jun 21, 2026
1965d56
fix(agent): scale tool-output budget to the model context window (#23…
kshitijk4poor Jun 21, 2026
1e0b3a2
fix(agent): reset stale token calibration on model switch (#23767)
kshitijk4poor Jun 21, 2026
afb0a4e
style: add explicit utf-8 encoding to version-cache read/write (PLW1514)
arminanton Jun 21, 2026
796f618
fix(telegram): keep chunk markers outside code fences
whoislikemiha Jun 21, 2026
9f67ba1
fix(agent): guard finalize_turn cleanup chain so it never drops the r…
teknium1 Jun 21, 2026
99233fa
fix(cli): persist sessions before shutdown
May 16, 2026
e499d69
feat(api-server): configurable concurrent-run cap to prevent DoS (#50…
teknium1 Jun 21, 2026
c7e8854
fix(tui): persist session messages on force-quit / signal shutdown
bogerman1 May 9, 2026
6984026
fix(browser): enable SSRF guard when terminal runs in container
liuhao1024 Jun 4, 2026
ed8f789
Merge pull request #50136 from NousResearch/fix/context-aware-tool-bu…
kshitijk4poor Jun 21, 2026
c6a0929
Merge pull request #50137 from NousResearch/fix/reset-calibration-on-…
kshitijk4poor Jun 21, 2026
3509be7
fix(compression): auto-compression triggers at minimum context length…
teknium1 Jun 21, 2026
03563da
fix(gateway): raise session-hygiene hard message limit 400 → 5000 (#5…
teknium1 Jun 21, 2026
31e59fe
fix(telegram): preserve newlines in rich slash-command output (#46070)
Tranquil-Flow Jun 21, 2026
a966932
fix(telegram): exempt tables from rich newline hard-breaks
teknium1 Jun 21, 2026
8a506ed
fix(auth): make load_pool() non-destructive for env-seeded credentials
yeyitech Jun 21, 2026
2f4f23f
fix(codex): bridge app-server item/started events to Telegram tool-pr…
teknium1 Jun 21, 2026
65a477f
feat(desktop): add Update now button to About panel (#50186)
OutThisLife Jun 21, 2026
ea056b0
fix(telegram): avoid rich messages for CJK text
tt-a1i Jun 21, 2026
9078b4b
fix(file): harden read_file device alias blocking
egilewski May 29, 2026
6f5f58e
fix: keep poll read-only for notify_on_complete watcher
de1tydev Apr 29, 2026
41ba90f
fix(process): keep CLI drain dedup after poll goes read-only (#10156)
teknium1 Jun 21, 2026
296b290
chore(release): add AUTHOR_MAP entry for de1tydev (#10158)
teknium1 Jun 21, 2026
b6a4638
fix(compressor): treat empty-content summary response as failure, not…
teknium1 Jun 21, 2026
3e354b6
fix(agent): preserve copilot routed headers
konsisumer Jun 4, 2026
41e0c10
fix(agent): route repeated-compression warning through _emit_status (…
teknium1 Jun 21, 2026
b17180d
fix(session): finalize owned SQLite session rows on AIAgent.close()
yeyitech Jun 21, 2026
9e4fe32
fix(session): opt the background-review fork out of session finalization
teknium1 Jun 21, 2026
d0de460
fix(tui): /compress shows a before/after summary (#46686)
teknium1 Jun 21, 2026
7bc6f18
fix(hindsight): skip local_embedded daemon when running as root
LehaoLin Jun 21, 2026
e0498bd
fix(bedrock): price Claude prompt-cache tokens in /usage (#50307)
teknium1 Jun 21, 2026
a18bae6
fix(config): redact api_key in config show/set output (#50245) (#50313)
teknium1 Jun 21, 2026
71274f2
fix(file): reject read_file line-numbered writeback
Beandon13 May 4, 2026
16899ae
test(file): update guard assertions for unified display-text message
teknium1 Jun 21, 2026
93ea9b0
fix(gateway): cap inbound media download size to prevent memory exhau…
sgaofen Jun 21, 2026
d19aabb
fix(gateway): persist in-flight transcript on restart/shutdown drain …
teknium1 Jun 21, 2026
3b56d3a
fix(security): redact secrets in kanban tool payloads before persistence
steveonjava May 24, 2026
6183e8c
fix(telegram): make Bot API 10.1 rich messages opt-in (default off)
0x0sec Jun 21, 2026
7a8c4fe
chore(release): add AUTHOR_MAP entry for #48422 salvage
teknium1 Jun 21, 2026
587b5b9
fix(backup): capture memory-provider state stored outside HERMES_HOME…
teknium1 Jun 21, 2026
e581740
fix(kanban): single-writer dispatch lock to prevent orphan-dispatcher…
teknium1 Jun 21, 2026
56255f8
fix(agent): stop delegate cascade from deleting the parent session
jackjin1997 Jun 19, 2026
7a131f7
fix(api-server): stop silently promising async delivery on stateless …
teknium1 Jun 21, 2026
2a45423
fix(photon): classify Envoy overflow errors as retryable; add typing …
JoaoMarcos44 Jun 21, 2026
9578e52
fix(photon): detect unexpected sidecar death and trigger reconnect
JoaoMarcos44 Jun 21, 2026
e267237
test(photon): cover overflow retry, typing cooldown, sidecar-crash de…
teknium1 Jun 21, 2026
def3f63
fix(file): anchor device symlink guard to task cwd
egilewski Jun 21, 2026
5e3e89c
feat(hindsight): configurable embedded daemon health grace timeout (#…
teknium1 Jun 21, 2026
475e81d
fix(web_server): use run_in_executor for gateway pre-warm and drain-t…
JoaoMarcos44 Jun 21, 2026
e580706
test(web_server): add integration tests for desktop boot handshake fix
JoaoMarcos44 Jun 21, 2026
6bbacc2
fix(desktop): make cold-start port-announcement deadline tolerant
teknium1 Jun 21, 2026
c0409a8
feat(gateway): typed send-error classification (SendResult.error_kind…
teknium1 Jun 21, 2026
565b7c8
fix(telegram): stop typing indicator lingering after final reply
Jun 21, 2026
7d9f6a2
chore(release): add AUTHOR_MAP entry for #48678 salvage
teknium1 Jun 21, 2026
9d883ac
feat(plugins): add ctx.profile_name for session-agnostic profile acce…
teknium1 Jun 21, 2026
e217fd4
feat(kanban): add task lifecycle plugin hooks (claimed/completed/bloc…
teknium1 Jun 21, 2026
b6d1072
fix(cli): branch new worktrees from the fresh remote tip, not stale l…
teknium1 Jun 21, 2026
9630ec6
fix(kanban): pin worker TERMINAL_CWD to the task workspace (#50348)
teknium1 Jun 21, 2026
84ba83b
fix(kanban): bound the cross-process init lock so connect() can't han…
teknium1 Jun 21, 2026
1f4c5ae
fix(kanban): honor kanban.auto_decompose toggle live, without a gatew…
teknium1 Jun 21, 2026
ae46699
fix(security): validate snapshot_id and file paths in restore_quick_s…
Apr 13, 2026
87615f4
test(backup): add regression tests for restore_quick_snapshot path tr…
memosr Jun 5, 2026
d164ed0
fix(kanban): make reclaim claim-lock-aware to stop task/run status de…
teknium1 Jun 21, 2026
d7737bf
docs(ui-tui): fix file paths, add billing command, update file map
alelpoan Jun 21, 2026
b6f03ab
docs(ui-tui): add billing.step_up.verification event + perfPane.tsx t…
teknium1 Jun 21, 2026
8e4d2fd
docs(plugins): document acting from hooks via ctx.profile_name + disp…
teknium1 Jun 21, 2026
7502d38
fix(windows): prefer cmd npm shim on PATH fallback
helix4u Jun 21, 2026
5b45fb2
fix(security): sanitize kanban markdown html
zapabob Jun 5, 2026
537ad9e
fix(cli): guard missing ui-tui workspace before TUI launch
konsisumer Jun 19, 2026
db097fb
fix(cli): auto-restore a deleted ui-tui workspace from git before TUI…
teknium1 Jun 21, 2026
6902eb3
fix(cli): make ZIP-update directory replace atomic so it can't delete…
teknium1 Jun 21, 2026
09a96ba
fix(gateway): pause Telegram typing before stream finalize
LeonSGP43 Jun 21, 2026
f726908
fix(desktop/windows): stop in-app update from cascading into a backen…
teknium1 Jun 21, 2026
a4b1554
fix(whatsapp): normalize bare phone targets to JIDs before bridge send
sgaofen Jun 21, 2026
ed3d12a
fix(security): fail-closed when WebSocket peer is empty in loopback mode
memosr Jun 5, 2026
99f3072
fix(model-switch): a failed in-place swap must be a no-op, not a dead…
teknium1 Jun 21, 2026
bb77a8b
fix(gateway): respawn unmapped Windows gateways after update (#50090)…
teknium1 Jun 21, 2026
824c9d3
fix(config): alias model.api_base -> model.base_url for custom provid…
teknium1 Jun 21, 2026
a9c8025
fix(approval): honor interrupt in blocking gateway approval wait (#8697)
panghuer023 Jun 21, 2026
4cff036
test(approval): regression for interrupt-unblocks-approval; AUTHOR_MAP
teknium1 Jun 21, 2026
b7f6cb9
fix(email): resolve IMAP/SMTP host from config and validate before co…
devorun Jun 20, 2026
e921c4f
chore(release): map devorun salvage author email
teknium1 Jun 21, 2026
f79e0a7
fix(email): mark missing-config as non-retryable + reject blank env v…
teknium1 Jun 21, 2026
b5b8a4c
fix(gateway): respect adapter decline of fresh-final to prevent doubl…
liuhao1024 Jun 21, 2026
ed96669
fix(security): handle IPv6 scope IDs in URL safety checks to prevent …
sprmn24 May 25, 2026
87ab373
test(url-safety): cover IPv6 scope-ID strip + fail-closed in URL guards
teknium1 Jun 21, 2026
6f0ecf3
fix(redact): mask all Authorization schemes and x-api-key style headers
devorun Jun 21, 2026
bb59075
Merge pull request #50398 from helix4u/fix/windows-npm-path-fallback
ethernet8023 Jun 21, 2026
c8eb7cf
fix: V-009 security vulnerability
orbisai0security Apr 14, 2026
3a6a43c
fix(V-009): reject path traversal in SessionEntry.from_dict and harde…
orbisai0security Jun 5, 2026
aa2aac6
fix(V-009): reject Windows drive-letter paths in session field valida…
orbisai0security Jun 5, 2026
4d4ba08
refactor(session): simplify traversal guard to a helper + logger, har…
teknium1 Jun 21, 2026
624580e
fix(browser): verify daemon identity before orphan reaper kills a PID…
teknium1 Jun 21, 2026
745c4db
feat(desktop/windows): show update-in-progress feedback before the de…
teknium1 Jun 21, 2026
7785655
fix(desktop): keep the floating composer in-bounds so it can't be los…
OutThisLife Jun 21, 2026
c11ae82
fix(codex): seed app-server sessions with configured cwd
jplew Jun 4, 2026
8fcb813
fix(security): harden smart approval guard against prompt injection
Dusk1e Jun 21, 2026
242ec45
fix(gateway): don't lazy-install SDKs for unconfigured platforms on s…
xxxigm Jun 21, 2026
29176ff
test(gateway): cover no eager platform install on startup sweep
xxxigm Jun 21, 2026
8baa4e9
feat(cli): add native Antigravity OAuth provider
pmos69 May 20, 2026
b7a912e
fix(antigravity): bake in public OAuth client + default project fallback
teknium1 Jun 21, 2026
37c37c9
fix(antigravity): register google-antigravity ProviderProfile + AUTHO…
teknium1 Jun 21, 2026
c768c4b
fix(antigravity): move model flow to model_setup_flows + stop bare-al…
teknium1 Jun 21, 2026
16aeba1
fix(desktop): clamp composer peel-off under cursor
OutThisLife Jun 21, 2026
bef1d3e
fix(desktop): filter undefined entries in AttachmentList to prevent r…
liuhao1024 Jun 21, 2026
84fcbbf
fix(security): quote HERMES_TIMEZONE in remote code execution to prev…
Dusk1e Apr 12, 2026
13ce811
fix: show desktop approval fallback (#46548)
itsflownium Jun 21, 2026
1ec4fcf
Merge pull request #50466 from NousResearch/bb/composer-popout-bounds
OutThisLife Jun 21, 2026
1f6994d
chore(release): add AUTHOR_MAP entry for #45205 salvage (EtherAura)
teknium1 Jun 21, 2026
e5e2583
fix(desktop): relaunch on Linux after in-app update instead of hangin…
EtherAura Jun 21, 2026
84e1d31
refactor(kanban): fold worker/orchestrator skills into injected guida…
teknium1 Jun 22, 2026
e447723
fix(process-registry): re-validate PID identity before killing host p…
valentt Jun 10, 2026
77fdbbf
fix(whatsapp): validate bridge PID identity before killing stale pidf…
valentt Jun 10, 2026
069ab40
fix(whatsapp): only kill LISTENers when freeing the bridge port, neve…
valentt Jun 10, 2026
615a8e6
fix(whatsapp): add missing re import + fix test import path after ada…
teknium1 Jun 21, 2026
0fb3b13
chore: add valentt to AUTHOR_MAP for #43846 salvage
teknium1 Jun 21, 2026
1cefc2a
test(whatsapp): fix port-spares-client test race (listen before annou…
teknium1 Jun 21, 2026
012f40c
fix(status): cross-platform start-time fingerprint via psutil fallback
teknium1 Jun 22, 2026
b6d2ac1
feat(mem0): add self-hosted support via MEM0_HOST / host config
Apr 21, 2026
452a725
fix(mem0): address PR review — restore docstrings, keep api_key required
May 4, 2026
73340d8
chore: add buihongduc132 to AUTHOR_MAP for mem0 salvage
teknium1 Jun 22, 2026
2b3a4f0
fix(agent): strip stale reasoning_content when falling back to a stri…
teknium1 Jun 22, 2026
0a7ae28
fix(compressor): remove logging.basicConfig from library class __init__
annguyenNous Jun 22, 2026
9bf9a9f
fix(swe-runner): move logging.basicConfig out of Runner __init__ into…
teknium1 Jun 22, 2026
7726ce3
fix(security): close hermes-0day MCP-persistence attack surface
teknium1 Jun 22, 2026
eb51c18
fix(docker): replace dashboard --insecure with basic-auth provider
teknium1 Jun 22, 2026
f45ace9
feat(security): startup security posture audit (warn-on-load)
teknium1 Jun 22, 2026
41fe086
style(security-audit): add explicit encoding to read_text calls (ruff…
teknium1 Jun 22, 2026
8cecaf0
feat(process): escalate SIGTERM->SIGKILL on host-pid termination afte…
teknium1 Jun 22, 2026
8cbb34b
chore: map tkwong co-author email for #15008 SIGKILL-escalation credit
teknium1 Jun 22, 2026
8cfcbd3
fix(process): SIGKILL the whole tree on escalation, not just wait_pro…
teknium1 Jun 22, 2026
5bf23ff
fix(banner): don't advertise toolsets/skills the agent wasn't given (…
teknium1 Jun 22, 2026
7130d60
feat(providers): remove google-gemini-cli + google-antigravity OAuth …
teknium1 Jun 22, 2026
0768ed3
docs(agents): fix stale platform adapter path in token-lock note
TutkuEroglu Jun 21, 2026
4c1934d
docs: repoint remaining stale gateway/platforms adapter refs to plugi…
teknium1 Jun 22, 2026
b0a2598
fix(terminal): make hermes install dir reachable in subshell PATH (#5…
teknium1 Jun 22, 2026
95d53c3
feat(cli): /reasoning full — show complete thinking, not 10-line clam…
teknium1 Jun 22, 2026
9e96e70
feat(cli): /prompt — compose your next prompt in $EDITOR (#50509)
teknium1 Jun 22, 2026
e448b21
feat(dashboard): interactive auth setup on no-provider non-loopback b…
teknium1 Jun 22, 2026
44dc4d7
Merge branch '_c_50033' into z50033
arminanton Jun 22, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -1175,7 +1175,7 @@ automatically scope to the active profile.
a unique credential (bot token, API key), call `acquire_scoped_lock()` from
`gateway.status` in the `connect()`/`start()` method and `release_scoped_lock()` in
`disconnect()`/`stop()`. This prevents two profiles from using the same credential.
See `gateway/platforms/telegram.py` for the canonical pattern.
See `plugins/platforms/irc/adapter.py` for the canonical pattern.

6. **Profile operations are HOME-anchored, not HERMES_HOME-anchored** — `_get_profiles_root()`
returns `Path.home() / ".hermes" / "profiles"`, NOT `get_hermes_home() / "profiles"`.
Expand Down
4 changes: 4 additions & 0 deletions acp_adapter/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -617,6 +617,10 @@ def _make_agent(

_register_task_cwd(session_id, cwd)
agent = AIAgent(**kwargs)
# Codex app-server sessions are spawned lazily on the first turn. Stamp
# the ACP workspace onto the agent so the Codex runtime starts from the
# editor/session cwd instead of the Hermes daemon's process cwd.
agent.session_cwd = cwd
# ACP stdio transport requires stdout to remain protocol-only JSON-RPC.
# Route any incidental human-readable agent output to stderr instead.
agent._print_fn = _acp_stderr_print
Expand Down
10 changes: 10 additions & 0 deletions agent/agent_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -808,6 +808,8 @@ def init_agent(
# _custom_headers; older/mocked clients may expose
# _default_headers instead.
_routed_headers = getattr(_routed_client, "_custom_headers", None)
if not _routed_headers:
_routed_headers = getattr(_routed_client, "default_headers", None)
if not _routed_headers:
_routed_headers = getattr(_routed_client, "_default_headers", None)
if _routed_headers:
Expand Down Expand Up @@ -861,6 +863,8 @@ def init_agent(
if _provider_timeout is not None:
client_kwargs["timeout"] = _provider_timeout
_fb_headers = getattr(_fb_client, "_custom_headers", None)
if not _fb_headers:
_fb_headers = getattr(_fb_client, "default_headers", None)
if not _fb_headers:
_fb_headers = getattr(_fb_client, "_default_headers", None)
if _fb_headers:
Expand Down Expand Up @@ -1096,6 +1100,12 @@ def init_agent(
agent._parent_session_id = parent_session_id
agent._last_flushed_db_idx = 0 # tracks DB-write cursor to prevent duplicate writes
agent._session_db_created = False # DB row deferred to run_conversation()
# Most agents own their session row and should finalize it on close().
# Some temporary helper agents (manual compression / session-hygiene /
# background-review forks) rotate or share the session forward to a
# continuation row that must remain open after the helper is torn down;
# those callers explicitly set this flag to False.
agent._end_session_on_close = True
agent._session_init_model_config = {
"max_iterations": agent.max_iterations,
"reasoning_config": reasoning_config,
Expand Down
120 changes: 69 additions & 51 deletions agent/agent_runtime_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -1378,22 +1378,6 @@ def create_openai_client(agent, client_kwargs: dict, *, reason: str, shared: boo
agent._client_log_context(),
)
return client
if agent.provider == "google-gemini-cli" or str(client_kwargs.get("base_url", "")).startswith("cloudcode-pa://"):
from agent.gemini_cloudcode_adapter import GeminiCloudCodeClient

# Strip OpenAI-specific kwargs the Gemini client doesn't accept
safe_kwargs = {
k: v for k, v in client_kwargs.items()
if k in {"api_key", "base_url", "default_headers", "project_id", "timeout"}
}
client = GeminiCloudCodeClient(**safe_kwargs)
_ra().logger.info(
"Gemini Cloud Code Assist client created (%s, shared=%s) %s",
reason,
shared,
agent._client_log_context(),
)
return client
if agent.provider == "gemini":
from agent.gemini_native_adapter import GeminiNativeClient, is_native_gemini_base_url

Expand Down Expand Up @@ -2187,25 +2171,36 @@ def copy_reasoning_content_for_api(agent, source_msg: dict, api_msg: dict) -> No
if source_msg.get("role") != "assistant":
return

# 1. Explicit reasoning_content already set — preserve it verbatim
# (includes DeepSeek/Kimi's own space-placeholder written at creation
# time, and any valid reasoning content from the same provider).
needs_thinking_pad = agent._needs_thinking_reasoning_pad()

# 1. Explicit reasoning_content already set.
#
# When the active provider enforces the thinking-mode echo-back
# (DeepSeek / Kimi / MiMo), preserve it verbatim — that includes their
# own space-placeholder written at creation time and any valid reasoning
# from the same provider. Sessions persisted BEFORE #17341 have
# empty-string placeholders pinned at creation time; DeepSeek V4 Pro
# rejects those with HTTP 400, so upgrade "" → " " on replay.
#
# Exception: sessions persisted BEFORE #17341 have empty-string
# placeholders pinned at creation time. DeepSeek V4 Pro rejects
# those with HTTP 400. When the active provider enforces the
# thinking-mode echo, upgrade "" → " " on replay so stale history
# doesn't 400 the user on the next turn.
# When the active provider does NOT enforce echo-back, strip the field
# entirely. Strict OpenAI-compatible providers (Mistral, Cerebras, Groq,
# SambaNova, …) reject ANY reasoning_content key in input messages with
# HTTP 400/422 ("Extra inputs are not permitted"), even an empty string
# or a single-space pad. This is the cross-provider fallback case: a
# reasoning primary (DeepSeek/Kimi/MiMo) pads history with " ", then a
# fallback to a strict provider replays that pad and 422s. Stripping
# here covers the rebuild path; reapply_reasoning_echo_for_provider()
# covers the already-built api_messages path. Refs #45655.
existing = source_msg.get("reasoning_content")
if isinstance(existing, str):
if existing == "" and agent._needs_thinking_reasoning_pad():
if not needs_thinking_pad:
api_msg.pop("reasoning_content", None)
elif existing == "":
api_msg["reasoning_content"] = " "
else:
api_msg["reasoning_content"] = existing
return

needs_thinking_pad = agent._needs_thinking_reasoning_pad()

# 2. Cross-provider poisoned history (#15748): on DeepSeek/Kimi,
# if the source turn has tool_calls AND a 'reasoning' field but no
# 'reasoning_content' key, the 'reasoning' text was written by a
Expand All @@ -2231,9 +2226,13 @@ def copy_reasoning_content_for_api(agent, source_msg: dict, api_msg: dict) -> No
# for providers that use the internal 'reasoning' key.
# This must happen before the unconditional empty-string fallback so
# genuine reasoning content is not overwritten (#15812 regression in
# PR #15478).
# PR #15478). Only promote for providers that enforce echo-back —
# strict providers reject the field (refs #45655).
if isinstance(normalized_reasoning, str) and normalized_reasoning:
api_msg["reasoning_content"] = normalized_reasoning
if needs_thinking_pad:
api_msg["reasoning_content"] = normalized_reasoning
else:
api_msg.pop("reasoning_content", None)
return

# 4. DeepSeek / Kimi thinking mode: all assistant messages need
Expand All @@ -2254,34 +2253,53 @@ def copy_reasoning_content_for_api(agent, source_msg: dict, api_msg: dict) -> No


def reapply_reasoning_echo_for_provider(agent, api_messages: list) -> int:
"""Re-pad assistant turns with reasoning_content for the active provider.
"""Re-pad (or strip) assistant turns' reasoning_content for the active provider.

``api_messages`` is built once, before the retry loop, while the *primary*
provider is active. If a mid-conversation fallback then switches to a
require-side provider (DeepSeek / Kimi / MiMo thinking mode), assistant
turns that were built when the prior provider did NOT need the echo-back go
out without ``reasoning_content`` and the new provider rejects them with
HTTP 400 ("The reasoning_content in the thinking mode must be passed back").

Calling this immediately before building the request kwargs re-applies the
pad against the *current* provider. It is idempotent and a no-op unless
``_needs_thinking_reasoning_pad()`` is True for the active provider, so it
is safe to call every iteration and covers every fallback path.

Returns the number of assistant turns that gained reasoning_content.
provider is active. A mid-conversation fallback can then switch providers,
so the reasoning fields baked into ``api_messages`` are shaped for the
*prior* provider and must be reconciled against the *current* one:

* Switching TO a require-side provider (DeepSeek / Kimi / MiMo thinking
mode): assistant turns built when the prior provider did NOT need the
echo-back go out without ``reasoning_content`` and the new provider
rejects them with HTTP 400 ("The reasoning_content in the thinking mode
must be passed back"). Re-apply the pad.

* Switching TO a strict provider that rejects the field (Mistral,
Cerebras, Groq, SambaNova, …): assistant turns built under a reasoning
primary carry a ``reasoning_content`` pad (often a single space ``" "``),
and the strict provider rejects it with HTTP 400/422 ("Extra inputs are
not permitted"). Strip the field. This is the exact cross-provider
fallback bug from #45655 — a DeepSeek primary pads history with ``" "``,
the request falls back to Mistral, and Mistral 422s on the stale pad.

Calling this immediately before building the request kwargs reconciles the
fields against the *current* provider. It is idempotent and safe to call
every iteration; it covers every fallback path.

Returns the number of assistant turns whose reasoning_content was added or
removed.
"""
if not agent._needs_thinking_reasoning_pad():
return 0
padded = 0
needs_pad = agent._needs_thinking_reasoning_pad()
changed = 0
for api_msg in api_messages:
if api_msg.get("role") != "assistant":
continue
if api_msg.get("reasoning_content"):
continue
copy_reasoning_content_for_api(agent, api_msg, api_msg)
if api_msg.get("reasoning_content"):
padded += 1
return padded
if needs_pad:
if api_msg.get("reasoning_content"):
continue
copy_reasoning_content_for_api(agent, api_msg, api_msg)
if api_msg.get("reasoning_content"):
changed += 1
else:
# Strict provider — strip any stale reasoning_content pad left
# over from a reasoning primary so the fallback request doesn't
# 400/422 on it.
if "reasoning_content" in api_msg:
api_msg.pop("reasoning_content", None)
changed += 1
return changed


def _iter_pool_sockets(client: Any):
Expand Down
7 changes: 7 additions & 0 deletions agent/background_review.py
Original file line number Diff line number Diff line change
Expand Up @@ -575,6 +575,13 @@ def _bg_review_auto_deny(command, description, **kwargs):
# if a future code path bypasses the cache.
review_agent.session_start = agent.session_start
review_agent.session_id = agent.session_id
# The fork shares the parent's live session_id (pinned above for
# prefix-cache parity). It is single-lifecycle and calls close()
# right after this run_conversation(); without opting out, close()
# would finalize the parent's still-active session row mid
# conversation (the review fires every ~10 turns). Leave session
# finalization to the real owner (CLI close / gateway reset / cron).
review_agent._end_session_on_close = False
# Never let the review fork compress. It shares the parent's
# session_id, so if it won a compression race it would rotate the
# parent into a NEW child that the gateway never adopts (the fork
Expand Down
77 changes: 76 additions & 1 deletion agent/codex_runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,61 @@
logger = logging.getLogger(__name__)


def _codex_note_to_tool_progress(note: dict) -> tuple[str, str, dict] | None:
"""Map a Codex app-server ``item/started`` notification to a Hermes
tool-progress event ``(tool_name, preview, args)``.

The Codex app-server runtime processes ``item/started`` notifications for
command execution, file changes, and MCP/dynamic tool calls, but never
surfaced them as Hermes tool-progress events — so gateways (Telegram, etc.)
showed no verbose "running X" breadcrumbs on this route while every other
provider did (#38835). Returns None for items that aren't tool-shaped.
"""
if not isinstance(note, dict) or note.get("method") != "item/started":
return None
params = note.get("params") or {}
item = params.get("item") or {}
if not isinstance(item, dict):
return None

item_type = item.get("type") or ""
if item_type == "commandExecution":
command = item.get("command") or ""
return "exec_command", command, {"command": command, "cwd": item.get("cwd") or ""}

if item_type == "fileChange":
changes = item.get("changes") or []
preview = "file changes"
if isinstance(changes, list) and changes:
paths = [
str(change.get("path"))
for change in changes
if isinstance(change, dict) and change.get("path")
]
if paths:
preview = ", ".join(paths[:3])
if len(paths) > 3:
preview += f", +{len(paths) - 3} more"
return "apply_patch", preview, {"changes": changes}

if item_type == "mcpToolCall":
server = item.get("server") or "mcp"
tool = item.get("tool") or "unknown"
args = item.get("arguments") or {}
if not isinstance(args, dict):
args = {"arguments": args}
return f"mcp.{server}.{tool}", tool, args

if item_type == "dynamicToolCall":
tool = item.get("tool") or "unknown"
args = item.get("arguments") or {}
if not isinstance(args, dict):
args = {"arguments": args}
return tool, tool, args

return None


def _coerce_usage_int(value: Any) -> int:
if isinstance(value, bool):
return 0
Expand Down Expand Up @@ -195,7 +250,9 @@ def run_codex_app_server_turn(
# Spawned on first turn, reused across turns, closed at AIAgent
# shutdown (see _cleanup hook).
if not hasattr(agent, "_codex_session") or agent._codex_session is None:
cwd = getattr(agent, "session_cwd", None) or os.getcwd()
from agent.runtime_cwd import resolve_agent_cwd

cwd = getattr(agent, "session_cwd", None) or str(resolve_agent_cwd())
# Approval callback: defer to Hermes' standard prompt flow if a
# CLI thread has installed one. Gateway / cron contexts get the
# codex-side fail-closed default.
Expand All @@ -204,9 +261,27 @@ def run_codex_app_server_turn(
approval_callback = _get_approval_callback()
except Exception:
approval_callback = None

def _on_codex_event(note: dict) -> None:
# Bridge Codex app-server item/started notifications to Hermes
# tool-progress so gateways show verbose "running X" breadcrumbs
# on this route too (#38835).
progress_callback = getattr(agent, "tool_progress_callback", None)
if progress_callback is None:
return
mapped = _codex_note_to_tool_progress(note)
if mapped is None:
return
tool_name, preview, args = mapped
try:
progress_callback("tool.started", tool_name, preview, args)
except Exception:
logger.debug("codex tool-progress callback raised", exc_info=True)

agent._codex_session = CodexAppServerSession(
cwd=cwd,
approval_callback=approval_callback,
on_event=_on_codex_event,
)

# NOTE: the user message is ALREADY appended to messages by the
Expand Down
Loading