Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
125 commits
Select commit Hold shift + click to select a range
8eac520
fix(desktop): keep configured MoA presets in model picker
giggling-ginger Jul 8, 2026
5829fe1
fix(kanban): failure diagnostics exempt done/archived tasks
OutThisLife Jul 9, 2026
e87c495
fix(kanban): spawn workers headless — TUI can never eat a worker run
OutThisLife Jul 9, 2026
aea570d
fix(kanban): clear failure/crash diagnostics while a retry is in flight
OutThisLife Jul 9, 2026
77db9d6
fix(kanban): explicit re-queue bypasses the recent-success respawn guard
OutThisLife Jul 9, 2026
b06e2f8
fix(kanban): no-TTY gate in _wants_tui_early — the actual worker-cras…
OutThisLife Jul 9, 2026
8e3f953
Merge pull request #61649 from NousResearch/bb/kanban-worker-headless
OutThisLife Jul 9, 2026
fe25806
fix(config): retain last-known-good config when config.yaml fails to …
teknium1 Jul 9, 2026
a23d507
fix(agent): stop switch_model from pairing new provider with stale ba…
JoaoMarcos44 Jul 8, 2026
1f57ed2
fix(export): escape tool-call name in HTML session export
Adolanium Jul 9, 2026
501616e
fix(cli): set HERMES_YOLO_MODE before plugin discovery at startup
bytesnail Jul 7, 2026
d2e64fc
fix(cli): widen --yolo env guarantee to the _prepare_agent_startup ch…
teknium1 Jul 9, 2026
c71d19c
fix(cron): id-less job no longer freezes the whole scheduler
Jul 9, 2026
8e2ce43
fix(cron): non-dict schedule no longer freezes the whole scheduler
necoweb3 Jul 9, 2026
26f040e
fix(cron): malformed next_run_at no longer freezes the scheduler
necoweb3 Jul 9, 2026
10c0d9b
fix(cron): contain any per-job exception in the due scan; harden as a…
teknium1 Jul 10, 2026
b298fd5
test(cli): deflake --accept-hooks position test — one driver, one imp…
teknium1 Jul 10, 2026
549b87c
fix(gateway): prevent hygiene compression from destroying archived tr…
AlexFucuson9 Jul 9, 2026
9cbac64
test(gateway): pin in-place compaction skipping the destructive rewrite
teknium1 Jul 9, 2026
79f1274
chore: AUTHOR_MAP entry for AlexFucuson9 (PR #61209 salvage)
teknium1 Jul 10, 2026
b5226ca
fix(memory): share one SQLite connection per holographic store database
adambiggs Jun 10, 2026
a801046
fix(memory): resolve() the shared-connection registry key; symlink test
teknium1 Jul 10, 2026
0a4b4d6
fix(agent): reapply provider headers after model switch
AlexFucuson9 Jul 9, 2026
c75789f
test: regression coverage for header reapplication on model switch
teknium1 Jul 10, 2026
bd16395
chore: add AlexFucuson9 to AUTHOR_MAP (PR #61347 salvage)
teknium1 Jul 10, 2026
bdecf0a
fix(cli): ignore empty config sections
yungchentang Jul 4, 2026
4661307
fix(config): widen empty-section guard to _deep_merge in load_config
teknium1 Jul 10, 2026
50c66b2
fix(gateway): ignore malformed config sections
sharziki Jun 6, 2026
a7f65e3
fix(gateway): tolerate scalar gateway config block
sharziki Jun 6, 2026
276542c
fix(qqbot): add is_reconnect param to QQAdapter.connect for gateway r…
luxuguang-leo Jun 26, 2026
0f8603c
test(gateway): regression: every adapter.connect() must accept is_rec…
lemonwan Jul 6, 2026
9c40fc2
chore: map QQBot fix contributor
teknium1 Jul 10, 2026
16b841b
docs: add gateway reconnect contract infographic
teknium1 Jul 10, 2026
540f901
chore: map reconnect contract contributor
teknium1 Jul 10, 2026
b5c655c
fix(cli): normalize role into a single CSS token for the message class
briandevans Jul 9, 2026
a4dd08a
fix(session-export): escape html tool call names
kohoj Jul 9, 2026
c49d51b
chore: AUTHOR_MAP entry for kohoj (PR #61667 salvage)
teknium1 Jul 10, 2026
b2e227d
fix(agent): handle YAML null value in context_length_cache
ygd58 Jun 16, 2026
881a952
test: regression coverage for null context_lengths key (#47135)
teknium1 Jul 10, 2026
4c03032
fix(cli): normalize malformed skills config in get_disabled_skills (#…
teknium1 Jul 10, 2026
8937121
fix(stt/tts): guard against null config subsections crashing with Att…
x7peeps Jun 16, 2026
3a39421
fix(stt/tts): widen null-subsection guards to all provider config reads
teknium1 Jul 10, 2026
1a47769
test: deflake CI and dev-machine flaky tests in bulk (11 tests, 10 fi…
teknium1 Jul 10, 2026
5693265
fix(web): don't crash on a null web/backend config value
HumphreySun98 Jul 8, 2026
df886d0
fix(tools_config): guard against None in known_plugin_toolsets config
Jun 26, 2026
c9d5491
chore: AUTHOR_MAP entries for HumphreySun98 + 17324393074 (PR #61142/…
teknium1 Jul 10, 2026
07271a6
fix(tools_config): widen null guard to known_plugin_toolsets write path
teknium1 Jul 10, 2026
949e4cb
fix(feishu): add extra_ua_tags=["channel"] to FeishuWSClient for grou…
luxuguang-leo Jun 26, 2026
651e632
fix(feishu): ship Channel signaling SDK support
teknium1 Jul 10, 2026
0b0f60b
docs: add Feishu group events infographic
teknium1 Jul 10, 2026
5e50f18
fix(agent): reject malformed tool call arguments (#61784)
teknium1 Jul 10, 2026
838aa74
fix(agent): guard .get(key, "").method() None dereference in adapters
AlexFucuson9 Jul 1, 2026
a0972b9
fix: widen None-deref guards to config-derived sibling sites + tests
teknium1 Jul 10, 2026
f8361d2
fix(tools): enforce registry result contract (#61787)
teknium1 Jul 10, 2026
ec0227b
fix(cron): isolate profile store paths by context
embwl0x Jul 10, 2026
f82c713
fix(cron): scope profile runtime during webhook fire
kshitijk4poor Jul 10, 2026
3eb937a
fix(cron): preserve composed reports when verify-on-stop exhausts budget
HexLab98 Jul 10, 2026
53231fb
test(cron): cover verify-on-stop iteration-limit exit normalization
HexLab98 Jul 10, 2026
cd7c203
fix(agent): preserve gated responses without masking failures
kshitijk4poor Jul 10, 2026
1453431
refactor(agent): scope pending fallback to verification
kshitijk4poor Jul 10, 2026
f46e764
fix(agent): clear stale intermediate acknowledgments
kshitijk4poor Jul 10, 2026
8fc80bc
test(agent): pin verification fallback edge cases
kshitijk4poor Jul 10, 2026
6abf195
fix(agent): keep pending verification behind exit provenance
kshitijk4poor Jul 10, 2026
c101207
feat(desktop): Hermes Cloud connection mode — one sign-in, agent disc…
benbarclay Jul 10, 2026
2d315d3
polish(desktop): normalize cloud-URL highlight match + correct signed…
OutThisLife Jul 10, 2026
0ff0974
refactor(desktop): DRY the cloud helpers
OutThisLife Jul 10, 2026
9cb2a8a
Merge pull request #60757 from giggling-ginger/bugfix/issue-hunt-2026…
OutThisLife Jul 10, 2026
3f8b220
fix(tools): resolve MSYS paths in file tools on Windows
OutThisLife Jul 10, 2026
703487d
feat(desktop): point the no-agents link at the Hermes Cloud instance-…
OutThisLife Jul 10, 2026
1c7f31a
simplify(desktop): hardcode the Hermes Cloud setup link
OutThisLife Jul 10, 2026
24f6ed5
simplify(desktop): inline the cloud setup link like the rest of the app
OutThisLife Jul 10, 2026
04ca34b
Merge pull request #61915 from NousResearch/bb/salvage-50488-msys-paths
OutThisLife Jul 10, 2026
24ea219
fix(gateway): offload session store calls off the event loop via asyn…
kenyonxu Jul 10, 2026
94c2a40
fix(gateway): offload both blocking sources in compression-in-flight …
kenyonxu Jul 10, 2026
08e9dcf
fix(gateway): move all I/O out of session_store._lock in get_or_creat…
kenyonxu Jul 10, 2026
9d38a23
fix(gateway): enforce one async SessionStore boundary
kshitijk4poor Jul 10, 2026
b3f77f5
fix(gateway): close SessionStore concurrency gaps
kshitijk4poor Jul 10, 2026
b196ce8
fix(gateway): unify routing save and reset races
kshitijk4poor Jul 10, 2026
ed36edd
test(gateway): recognize awaited reset result
kshitijk4poor Jul 10, 2026
57dfebe
fix(desktop): re-apply UI zoom on show/restore, scoped to chat windows
OutThisLife Jul 10, 2026
1318cd9
Merge pull request #61925 from NousResearch/bb/salvage-61245-ui-zoom
OutThisLife Jul 10, 2026
301acc9
fix(web): paste/drop images into dashboard Chat via HERMES_HOME/images
OutThisLife Jul 10, 2026
678be9f
Merge pull request #61929 from NousResearch/bb/salvage-57912-dash-paste
OutThisLife Jul 10, 2026
6207d68
fix(desktop): prevent bootstrap stale commit repin on existing checkouts
OutThisLife Jul 10, 2026
46cf87b
Merge pull request #61935 from NousResearch/bb/salvage-59902-bootstra…
OutThisLife Jul 10, 2026
5f00f36
fix(model): probe no-key custom provider catalogs
ooiuuii Jun 29, 2026
0629caa
fix(model): derive catalog policy from declarations
kshitijk4poor Jul 10, 2026
7628f27
test(model): assert explicit catalogs never probe
kshitijk4poor Jul 10, 2026
69f1460
test(model): isolate custom provider discovery
kshitijk4poor Jul 10, 2026
3aeaf37
fix(nous): forward provider routing through Portal
helix4u Jul 10, 2026
a0032f5
fix(routing): preserve profile and delegation parity
kshitijk4poor Jul 10, 2026
b3bde1f
feat(desktop): soft gateway switch + gateway-settings polish
OutThisLife Jul 10, 2026
79a104d
Merge pull request #61912 from NousResearch/bb/salvage-55402-desktop-…
OutThisLife Jul 10, 2026
a9f3f08
Merge pull request #61916 from NousResearch/bb/desktop-gateway-switch-ux
OutThisLife Jul 10, 2026
29f3dc0
fix(desktop): stop Tip from sticking open and blocking clicks
OutThisLife Jul 10, 2026
caf557b
Merge pull request #61973 from NousResearch/bb/desktop-tip-stuck
OutThisLife Jul 10, 2026
ca65135
fix(auth): recover runtime Nous token from shared store
Jul 7, 2026
03b8a00
fix(auth): recompute Nous routing after shared recovery
kshitijk4poor Jul 10, 2026
0e67c72
fix(auth): validate and persist shared Nous routing
kshitijk4poor Jul 10, 2026
8fa0d8b
test(auth): pin runtime routing persistence on failure
kshitijk4poor Jul 10, 2026
dfdc315
fix(models): remove unavailable OpenCode Zen free models (#61163)
gigakun3030 Jul 10, 2026
623165a
fix(tui): discover MCP tools in slash workers
HexLab98 Jul 10, 2026
d37090a
test(tui): cover profile-local MCP discovery
kshitijk4poor Jul 10, 2026
304cdbd
fix(curator): forward credential pool from runtime resolution
infinitycrew39 Jul 9, 2026
f39c88b
test(curator): assert review fork forwards pool and overrides
infinitycrew39 Jul 9, 2026
97e9c64
fix(runtime): preserve resolved fork metadata
kshitijk4poor Jul 10, 2026
8727e67
fix(runtime): preserve resolved fork metadata
kshitijk4poor Jul 10, 2026
7ae9fae
fix(tools): handle dict URLs in web_extract display and tool processing
liuhao1024 Jul 9, 2026
de33c24
fix(web): harden extract input and display boundaries
kshitijk4poor Jul 10, 2026
e640bb5
test(web): cover model-facing dict URL dispatch
kshitijk4poor Jul 10, 2026
459cf34
fix(web): preserve extract result input order
kshitijk4poor Jul 10, 2026
c2a40b2
fix(web): handle short extract provider results
kshitijk4poor Jul 10, 2026
0b753d8
fix(display): harden fallback label formatting
kshitijk4poor Jul 10, 2026
54e1864
fix(acp): unwrap web extract object titles
kshitijk4poor Jul 10, 2026
97fb9e1
fix(telegram): classify PTB heartbeat transport errors
liuhao1024 Jul 10, 2026
90bd5b0
test(telegram): mirror PTB errors in heartbeat recovery
kshitijk4poor Jul 10, 2026
9b72995
fix(cron): never stale-remove a one-shot whose run is still alive
PRATHAMESH75 Jul 10, 2026
cd7a8df
fix(agent): release pool FDs on owning-thread client close (#61979)
giggling-ginger Jul 10, 2026
1a2f3ae
fix tui finalize persist drop conversation_history so disconnect save…
WilsonKinyua Jul 10, 2026
35d777d
chore: map WilsonKinyua release attribution
teknium1 Jul 10, 2026
b9b463f
feat(security): expose deterministic tool output risk (#61793)
teknium1 Jul 10, 2026
db8772a
fix(desktop): don't emit js files when we build desktop
ethernet8023 Jul 10, 2026
b8880f1
fix(desktop): type-check electron/ in CI typecheck
ethernet8023 Jul 10, 2026
7cdd3ba
feat(mcp-catalog): add Unstructured Transform MCP server
ctrahey Jul 10, 2026
7c9dfd4
docs(mcp-catalog): make unstructured token-path note profile-aware
ctrahey Jul 13, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion acp_adapter/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,12 @@ def build_tool_title(tool_name: str, args: Dict[str, Any]) -> str:
if tool_name == "web_extract":
urls = args.get("urls", [])
if urls:
return f"extract: {urls[0]}" + (f" (+{len(urls)-1})" if len(urls) > 1 else "")
first = urls[0]
if isinstance(first, dict):
first = first.get("url") or first.get("href") or "?"
elif not isinstance(first, str):
first = "?"
return f"extract: {first}" + (f" (+{len(urls)-1})" if len(urls) > 1 else "")
return "web extract"
if tool_name == "process":
action = str(args.get("action") or "").strip() or "manage"
Expand Down
76 changes: 54 additions & 22 deletions agent/agent_runtime_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -1816,13 +1816,30 @@ def switch_model(agent, new_model, new_provider, api_key='', base_url='', api_mo
# ── Swap core runtime fields ──
agent.model = new_model
agent.provider = new_provider
# Use new base_url when provided; only fall back to current when the
# new provider genuinely has no endpoint (e.g. native SDK providers).
# Without this guard the old provider's URL (e.g. Ollama's localhost
# address) would persist silently after switching to a cloud provider
# that returns an empty base_url string.
# Use the new base_url when provided. When it's empty AND the
# provider is actually changing, do NOT fall back to the current
# (old provider's) URL — that silently pairs the new provider label
# with the previous provider's endpoint (e.g. new_provider=minimax
# paired with the leftover api.githubcopilot.com URL), and every
# request after the switch 400s at the wrong host. This mismatched
# pair also gets snapshotted into _primary_runtime below, so it
# keeps re-applying on every subsequent turn until a full restart.
# Fail loud instead: the caller (model_switch.switch_model())
# already resolves base_url for every real provider, so an empty
# value here means resolution failed upstream, not that the
# provider genuinely has none. Re-selecting the SAME provider with
# an empty base_url (e.g. a credential-only refresh) is still fine
# to keep the current URL. See #47828.
old_norm_provider = (old_provider or "").strip().lower()
new_norm_provider = (new_provider or "").strip().lower()
if base_url:
agent.base_url = base_url
elif old_norm_provider != new_norm_provider:
raise ValueError(
f"switch_model: no base_url resolved for provider "
f"'{new_provider}' (switching from '{old_provider}'); "
"refusing to keep the previous provider's endpoint"
)
agent.api_mode = api_mode
# Invalidate transport cache — new api_mode may need a different transport
if hasattr(agent, "_transport_cache"):
Expand Down Expand Up @@ -1936,6 +1953,11 @@ def switch_model(agent, new_model, new_provider, api_key='', base_url='', api_mo
_sm_timeout = get_provider_request_timeout(agent.provider, agent.model)
if _sm_timeout is not None:
agent._client_kwargs["timeout"] = _sm_timeout
# Reapply provider-specific headers (e.g. OpenRouter HTTP-Referer,
# X-Title) that were lost when _client_kwargs was rebuilt from
# scratch. Without this, model switches clear attribution headers
# and OpenRouter logs show "Unknown" for subsequent requests.
agent._apply_client_headers_for_base_url(effective_base)
agent.client = agent._create_openai_client(
dict(agent._client_kwargs),
reason="switch_model",
Expand Down Expand Up @@ -3139,21 +3161,20 @@ def apply_pending_steer_to_tool_results(agent, messages: list, num_tool_msgs: in



def force_close_tcp_sockets(client: Any) -> int:
"""Abort in-flight TCP I/O by shutting down sockets WITHOUT closing FDs.
def force_close_tcp_sockets(client: Any, *, release_fds: bool = False) -> int:
"""Abort in-flight TCP I/O by shutting down pool sockets.

When a provider drops a connection mid-stream — or the user issues an
interrupt — we want to unblock httpx's reader/writer immediately rather
than waiting for the kernel's per-connection timeout. ``shutdown(SHUT_RDWR)``
achieves that: it sends FIN, breaks any pending ``recv``/``send`` with EOF
or ``EPIPE``, but does NOT release the file descriptor.
or ``EPIPE``.

Historically this helper also called ``socket.close()`` so the FD got
released immediately, but that's unsafe when (as is the case for both the
interrupt-abort path and stale-call kill path) the helper runs on a
different thread than the one driving the request:
By default (``release_fds=False``) this helper does **not** call
``socket.close()`` / release the FD. That default is load-bearing for
cross-thread abort paths (#29507):

* The Python ``socket.socket`` we close here is the SAME object held by
* The Python ``socket.socket`` we close is the SAME object held by
httpx's pool, so closing it via Python sets its ``_fd`` to -1 and
future operations on that Python object fail safely.
* BUT the SSL wrapper (``ssl.SSLSocket``'s underlying OpenSSL ``BIO``)
Expand All @@ -3165,15 +3186,20 @@ def force_close_tcp_sockets(client: Any) -> int:
wrong file (issue #29507: 24-byte TLS application-data record
clobbering SQLite header bytes 5..28).

The fix is to let the owning thread own the close. ``shutdown()`` from any
thread is FD-safe; ``close()`` is not. The httpx connection's own close
path — which runs from the worker thread when it unwinds — will release
the FD via the same ``socket.socket`` object, and because Python's socket
close atomically swaps ``_fd`` to -1 *before* issuing ``os.close``, there
is no FD-aliasing window when only one thread closes.
``shutdown()`` from any thread is FD-safe; ``close()`` is not when a
stranger thread still has the BIO holding the raw FD.

When the **owning** thread is disposing of a client that is no longer
shared (``_close_openai_client`` after replace / request-complete), pass
``release_fds=True``. httpx's own ``client.close()`` does not reliably
``os.close()`` sockets that were already ``shutdown()``'d, so without an
explicit ``sock.close()`` those FDs stay in kernel CLOSED state forever
and accumulate under long-lived gateways (issue #61979 — ~1 CLOSED fd
per ~6 minutes through a local proxy path).

Returns the number of sockets shut down. (Field kept as
``tcp_force_closed=N`` in the log line for backwards-compatible parsing.)
Returns the number of sockets shut down (and optionally closed). Field
kept as ``tcp_force_closed=N`` in log lines for backwards-compatible
parsing.
"""
import socket as _socket

Expand All @@ -3185,7 +3211,13 @@ def force_close_tcp_sockets(client: Any) -> int:
except OSError:
# Already shut down / not connected / FD invalid — all benign.
pass
# IMPORTANT (#29507): do NOT call sock.close() here. See docstring.
# IMPORTANT (#29507): never release FDs from stranger-thread
# abort paths. Only the owning-thread close path may opt in.
if release_fds:
try:
sock.close()
except OSError:
pass
shutdown_count += 1
except Exception as exc:
_ra().logger.debug("Force-close TCP sockets sweep error: %s", exc)
Expand Down
2 changes: 1 addition & 1 deletion agent/anthropic_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -2102,7 +2102,7 @@ def _convert_user_message(content: Any) -> Dict[str, Any]:
if isinstance(content, list):
converted_blocks = _convert_content_to_anthropic(content)
if not converted_blocks or all(
b.get("text", "").strip() == ""
(b.get("text") or "").strip() == ""
for b in converted_blocks
if isinstance(b, dict) and b.get("type") == "text"
):
Expand Down
4 changes: 2 additions & 2 deletions agent/auxiliary_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -4716,8 +4716,8 @@ def _wrap_if_needed(client_obj, final_model_str: str, base_url_str: str = "",
if custom_entry is None:
custom_entry = _get_named_custom_provider(provider)
if custom_entry:
custom_base = custom_entry.get("base_url", "").strip()
custom_key = custom_entry.get("api_key", "").strip()
custom_base = (custom_entry.get("base_url") or "").strip()
custom_key = (custom_entry.get("api_key") or "").strip()
custom_key_env = (custom_entry.get("key_env") or custom_entry.get("api_key_env") or "").strip()
if not custom_key and custom_key_env:
custom_key = os.getenv(custom_key_env, "").strip()
Expand Down
22 changes: 20 additions & 2 deletions agent/background_review.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,11 @@ def _resolve_review_runtime(agent: Any) -> Dict[str, Any]:
"api_key": parent_runtime.get("api_key") or None,
"base_url": parent_runtime.get("base_url") or None,
"api_mode": parent_api_mode,
"credential_pool": getattr(agent, "_credential_pool", None),
"request_overrides": dict(getattr(agent, "request_overrides", {}) or {}),
"max_tokens": getattr(agent, "max_tokens", None),
"command": getattr(agent, "acp_command", None),
"args": list(getattr(agent, "acp_args", []) or []),
"routed": False,
}
try:
Expand Down Expand Up @@ -89,10 +94,15 @@ def _resolve_review_runtime(agent: Any) -> Dict[str, Any]:
)
return {
"provider": rp.get("provider") or task_provider,
"model": task_model,
"model": rp.get("model") or task_model,
"api_key": rp.get("api_key"),
"base_url": rp.get("base_url"),
"api_mode": rp.get("api_mode"),
"credential_pool": rp.get("credential_pool"),
"request_overrides": dict(rp.get("request_overrides") or {}),
"max_tokens": rp.get("max_output_tokens"),
"command": rp.get("command"),
"args": list(rp.get("args") or []),
"routed": True,
}
except Exception as e:
Expand Down Expand Up @@ -680,6 +690,12 @@ def _bg_review_auto_deny(command, description, **kwargs):
# Match parent's toolset config so ``tools[]`` is byte-identical
# in the request body — Anthropic's cache key includes it.
# (The runtime whitelist below still restricts dispatch.)
_fork_kwargs: Dict[str, Any] = {}
if isinstance(_rt.get("max_tokens"), int):
_fork_kwargs["max_tokens"] = _rt["max_tokens"]
if isinstance(_rt.get("command"), str) and _rt["command"]:
_fork_kwargs["acp_command"] = _rt["command"]
_fork_kwargs["acp_args"] = _rt.get("args") or []
review_agent = AIAgent(
model=_rt.get("model") or agent.model,
max_iterations=16,
Expand All @@ -689,11 +705,13 @@ def _bg_review_auto_deny(command, description, **kwargs):
api_mode=_rt.get("api_mode"),
base_url=_rt.get("base_url") or None,
api_key=_rt.get("api_key") or None,
credential_pool=getattr(agent, "_credential_pool", None),
credential_pool=_rt.get("credential_pool"),
request_overrides=_rt.get("request_overrides") or {},
parent_session_id=agent.session_id,
enabled_toolsets=getattr(agent, "enabled_toolsets", None),
disabled_toolsets=getattr(agent, "disabled_toolsets", None),
skip_memory=True,
**_fork_kwargs,
)
review_agent._memory_write_origin = "background_review"
review_agent._memory_write_context = "background_review"
Expand Down
70 changes: 43 additions & 27 deletions agent/chat_completion_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,25 @@ def _validated_openrouter_provider_sort(raw_sort: Any) -> Optional[str]:
return None


def _provider_preferences_for_agent(agent) -> Dict[str, Any]:
"""Build the validated provider-routing object shared by request paths."""
preferences: Dict[str, Any] = {}
if agent.providers_allowed:
preferences["only"] = agent.providers_allowed
if agent.providers_ignored:
preferences["ignore"] = agent.providers_ignored
if agent.providers_order:
preferences["order"] = agent.providers_order
provider_sort = _validated_openrouter_provider_sort(agent.provider_sort)
if provider_sort:
preferences["sort"] = provider_sort
if agent.provider_require_parameters:
preferences["require_parameters"] = True
if agent.provider_data_collection:
preferences["data_collection"] = agent.provider_data_collection
return preferences


def _env_float(name: str, default: float) -> float:
try:
return float(os.getenv(name, str(default)))
Expand Down Expand Up @@ -801,21 +820,8 @@ def build_api_kwargs(agent, api_messages: list) -> dict:
_omit_temp = False
_fixed_temp = None

# Provider preferences (OpenRouter-style)
_prefs: Dict[str, Any] = {}
if agent.providers_allowed:
_prefs["only"] = agent.providers_allowed
if agent.providers_ignored:
_prefs["ignore"] = agent.providers_ignored
if agent.providers_order:
_prefs["order"] = agent.providers_order
_provider_sort = _validated_openrouter_provider_sort(agent.provider_sort)
if _provider_sort:
_prefs["sort"] = _provider_sort
if agent.provider_require_parameters:
_prefs["require_parameters"] = True
if agent.provider_data_collection:
_prefs["data_collection"] = agent.provider_data_collection
# Provider preferences (aggregator profile decides whether to emit them).
_prefs = _provider_preferences_for_agent(agent)

# Anthropic-compatible max-output fallback (last resort only — applied in
# build_kwargs *after* ephemeral/user/profile max_tokens, never overriding
Expand Down Expand Up @@ -1690,18 +1696,28 @@ def handle_max_iterations(agent, messages: list, api_call_count: int) -> str:
if _lm_reasoning_effort is not None:
summary_kwargs["reasoning_effort"] = _lm_reasoning_effort

# Include provider routing preferences
provider_preferences = {}
if agent.providers_allowed:
provider_preferences["only"] = agent.providers_allowed
if agent.providers_ignored:
provider_preferences["ignore"] = agent.providers_ignored
if agent.providers_order:
provider_preferences["order"] = agent.providers_order
_provider_sort = _validated_openrouter_provider_sort(agent.provider_sort)
if _provider_sort:
provider_preferences["sort"] = _provider_sort
if provider_preferences and (
# Merge the profile's canonical body even when routing is unset:
# profiles may always emit required metadata such as Portal tags.
provider_preferences = _provider_preferences_for_agent(agent)
profile_extra_body = {}
try:
from providers import get_provider_profile

provider_profile = get_provider_profile(agent.provider)
if provider_profile is not None:
profile_extra_body = provider_profile.build_extra_body(
session_id=getattr(agent, "session_id", None),
provider_preferences=provider_preferences or None,
model=agent.model,
base_url=agent.base_url,
reasoning_config=agent.reasoning_config,
)
except Exception:
pass

if profile_extra_body:
summary_extra_body.update(profile_extra_body)
if provider_preferences and "provider" not in profile_extra_body and (
(agent.provider or "").strip().lower() == "openrouter"
or agent._is_openrouter_url()
):
Expand Down
11 changes: 10 additions & 1 deletion agent/coding_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
import os
import re
import subprocess
import tempfile
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Optional
Expand Down Expand Up @@ -412,10 +413,18 @@ def _marker_root(cwd: Path) -> Optional[Path]:
"""
current = cwd.resolve()
home = _home()
# Shared world-writable temp roots are never project roots: a stray
# manifest in /tmp (left by any process) must not flip every session
# whose cwd lives under the temp dir into the coding posture. Same
# reasoning as the $HOME skip below.
try:
temp_root = Path(tempfile.gettempdir()).resolve()
except Exception:
temp_root = None
for depth, parent in enumerate([current, *current.parents]):
if depth > 6:
break
if parent == home:
if parent == home or (temp_root is not None and parent == temp_root):
continue
for marker in _PROJECT_MARKERS:
if (parent / marker).exists():
Expand Down
18 changes: 18 additions & 0 deletions agent/conversation_loop.py
Original file line number Diff line number Diff line change
Expand Up @@ -613,6 +613,11 @@ def run_conversation(
truncated_response_parts: List[str] = []
compression_attempts = 0
_turn_exit_reason = "unknown" # Diagnostic: why the loop ended
# Last composed answer intentionally held back by a verification gate. If
# that continuation consumes the remaining budget, this is the best
# user-facing result available; it must not be confused with error or
# recovery text produced by unrelated exit paths.
_pending_verification_response = None

# Per-turn tally of consecutive successful credential-pool token refreshes,
# keyed by (provider, pool-entry-id). A persistent upstream 401 lets
Expand Down Expand Up @@ -5099,6 +5104,10 @@ def _perform_api_call(next_api_kwargs):
}
messages.append(continue_msg)
agent._session_messages = messages
# An acknowledgment is explicitly non-final. Do not let its
# text suppress iteration-limit summarization if this
# continuation consumes the remaining budget.
final_response = None
continue

codex_ack_continuations = 0
Expand Down Expand Up @@ -5173,6 +5182,12 @@ def _perform_api_call(next_api_kwargs):
# terminal. Keep a debug breadcrumb in agent.log for tracing.
logger.debug("verification stop-loop nudge issued (attempt %d)",
agent._verification_stop_nudges)
# Keep the attempted answer only as an explicit fallback for
# continuation-budget exhaustion. ``final_response`` itself
# must be cleared so the finalizer can distinguish this gate
# from unrelated error/recovery exits. (#61631)
_pending_verification_response = final_response
final_response = None
continue

# User verification-loop gate: when the agent edited code this
Expand Down Expand Up @@ -5224,6 +5239,8 @@ def _perform_api_call(next_api_kwargs):
agent._session_messages = messages
logger.debug("pre_verify nudge issued (attempt %d)",
agent._pre_verify_nudges)
_pending_verification_response = final_response
final_response = None
continue

messages.append(final_msg)
Expand Down Expand Up @@ -5308,6 +5325,7 @@ def _perform_api_call(next_api_kwargs):
original_user_message=original_user_message,
_should_review_memory=_should_review_memory,
_turn_exit_reason=_turn_exit_reason,
_pending_verification_response=_pending_verification_response,
)


Expand Down
Loading