The unavailable-provider warning was deduped by provider name process-wide,
so a multiplexed gateway or Desktop backend told only the first profile.
It is now keyed by (home, provider).
A first-time install refused at the consent gate said "Reinstall declined
... The installed plugin and active environment are unchanged" although
nothing was installed; migration notices now show that text to users.
Agent-start recovery (recover_at_startup) reported installed / refused / failed /
not-in-catalog only to agent.log, and so did a selected provider that reports unavailable,
so a user with security.allow_lazy_installs: false, offline, or who declined the dependency
prompt silently ran without external memory (#119769).
- Startup outcomes go through _emit_startup_warning: printed in the CLI; other drivers get
a sticky warn AgentNotice (Desktop toasts notices but drops warn-kind status.update), and
the messaging gateway, which wires notice_callback per turn, receives it on turn 1.
- hermes update labels each line with the profile it is about and gives the exact
`hermes -p <profile> plugins install <name>` command for non-active homes.
- Profiles sharing one environment get one set of dependency answers per provider
(shared_dependency_answers): the first install asks the normal Python/Node questions,
later homes reuse them; a decline or failure names the remaining profiles once instead of
failing (or re-prompting) one by one; Ctrl-C ends the migration with a message (#125794).
- The success line no longer names memory.<name> (#124038); docs follow.
Co-authored-by: chelsealong <chelsealong@126.com>
The session-start workspace snapshot runs git status, which re-hashes a
re-timestamped file through the repo-named clean filter, and a delegated
subagent's worktree add runs the smudge filter. Both now use
noninteractive_repo_git_env and skip the call when filter discovery fails.
(cherry picked from commit 04170a83d19a026fe6c5f968ce6e61b61f53c418)
The salvage moved generated images/videos to cache/generated/<kind>/ so the
24h sweep stops deleting the only copy (#126445), but two consumers still
only knew the old swept dirs:
- Strict media delivery (HERMES_MEDIA_DELIVERY_STRICT=1) allowlists cache
roots via _MEDIA_DELIVERY_CACHE_SUBDIRS; without "generated" a day-old
generated image was rejected once the recency window lapsed, i.e. the
move broke re-sending artifacts on public gateways.
- iter_cache_files (Modal/remote resync) walked the new never-swept tree in
full, so every sync re-uploaded the whole generation history. Only files
within the sweep window (MEDIA_CACHE_MAX_AGE_HOURS) are synced now.
GENERATED_SUBDIR and MEDIA_CACHE_MAX_AGE_HOURS live in agent/provider_media
as the single source for the image/video writers, the gateway allowlist and
cleanup default, and credential_files. Also trims duplicated comments and
fixes the pet orchestrate comments (nothing prunes cache/generated/images).
Tests use direct function refs instead of importlib strings.
Generated image/video output now writes under cache/generated/images|videos
instead of the swept inbound caches (cache/images, cache/videos). The hourly
gateway housekeeping deletes 24h-old files in those dirs; for base64-returning
providers the swept file was the only copy, breaking transcript references,
the Artifacts panel and later edits.
Rebased onto current main, where the writers delegate to agent/provider_media,
so the media kind is now the generated/ subpath. cache/generated is added to
tools/credential_files._CACHE_DIRS: without a mount/sync entry the Docker,
SSH and Modal backends never receive the bytes and the provider result loses
agent_visible_image. All callers consume the returned absolute paths, so no
call-site changes were needed; inbound download paths (get_image_cache_dir /
get_video_cache_dir) and the sweeper are untouched.
RED: tests/agent/test_generated_media_durable.py (9 failed on main) — 25h-backdated
generated files deleted by the real cleanup_image_cache / cleanup_video_cache, and
no mount mapping for cache/generated. tests/tools/test_image_generation_artifacts.py
::test_real_generated_image_is_agent_visible_under_ssh_env covers the sandbox
regression with the real writer. Green: 533 passed, 19 skipped.
Co-authored-by: Froraut <79215537+Froraut@users.noreply.github.com>
(cherry picked from commit fd23181f22232f66aec7076d6bf0868ed45661f2)
The agent flush row (_db_flush_row) always sets token_count, None for a replayed
message, so keying the keep on the key's absence missed the main production writer
and a digest-matched rewrite still nulled the stored count. Key it on the value.
Replay dicts from _rows_to_conversation never decode token_count, so the
serialized row always carried NULL. On the unaddressed (message_uid + CAS
snapshot) path that meant every legitimate rewrite nulled the original row's
token_count, and a dict whose content was unchanged still differed in
token_count and triggered a needless _rewrite_row. A missing key means
"unknown", so keep the stored value before comparing owned columns.
Also reuse message_metadata.message_uid_or_none at the lookup call site instead
of a second hand-rolled uid guard, and reflow an over-wide comment.
Stamping DB_ROW_SNAPSHOT on every repair_alternation load also moved the
include_row_ids=True resume loaders (resume conversations, rewind, TUI
gateway, turn lease) onto the CAS-match rewrite path. That path writes
every owned column from the live dict, and replay never decodes
token_count, so a rewrite of a resumed row nulled the stored
token_count. Only the unaddressed live replay needs the digest (it is
the proof that recovers a lost _row_id), so stamp it only when
repair_alternation and not include_row_ids.
The token_count column stays in _CONVERSATION_ROW_COLUMNS: the digest
hashes every owned column of these rows, so the projection must stay a
superset of _OWNED_COLUMNS; the comment now states that contract.
Also index (session_id, message_uid) over active rows so the logical
identity lookup, which runs inside the write lock, is a SEARCH instead
of a full-session scan; tighten the twin test so a copied uid without a
snapshot is still not adopted.
RunawayStreamWatch.feed trimmed its held text whenever two tail windows
had piled up, tracked by a separate _held counter. Since checks now run
at most one tail window apart and every check trims to the tail, the
held text can never exceed two tail windows plus the latest delta
without that branch, so the extra trim, the _held slot and its
bookkeeping are dead weight. The class docstring now states the bound
the check's own trim provides.
The list-and-any() form and the `or ""` guard added nothing: feed()
already ignores empty text, and stopping at the first watch that flags
the loop is the intended behaviour.
RunawayStreamWatch doubled the gap between checks, so a loop that began
after N legitimate characters streamed roughly N more before any check
saw it (100K of prose let ~156K of loop through). Each check already
reads only the 64K tail, so after the 16K -> 32K -> 64K start the watch
now checks every tail window; total work stays linear and the cut lands
within about 100K characters of the loop starting. The provider docs
promised a cut at 16,000 characters, which was never true for a loop
behind a long prefix; they now state the real bound.
The loop-cut test gains a row streaming ~100K distinct characters before
the loop and stalling once 100K loop characters have been sent, so a
watch that waits for the reply to double hangs the turn and fails.
The watch trimmed its text only when it ran a check, and the gap between
checks doubles, so between two checks it held everything that streamed:
about one tail plus the reply size at the last check. It now trims each
time a tail's worth of new text has arrived, so it holds at most two
tails plus the incoming chunk while the check schedule stays the same.
On the chat wire the watch read the reasoning text chosen for display,
which is the structured details whenever a chunk carries them. The plain
reasoning field is persisted either way, so a loop there went unseen
when details streamed beside it. Each representation has its own watch.
From review on #127242.
(cherry picked from commit 53dcaecc960238d4d0461bd43e80465182cbd098)
A looping model kept a turn streaming for as long as an uncapped endpoint
sent tokens: every repetition check ran after the stream ended, the stale
detector only fires on silence, and no max_tokens reaches these endpoints
by design. The stop path's own criterion (is_runaway_repetition at >= 16k
chars) now runs on the raw content and reasoning channels of the
chat-completions and Anthropic Messages streams, at 16k and every doubling
on a bounded tail. A hit closes the stream on its owner thread and ends the
turn through the repetition stop, without continuation.
Test loops are the reported incidents from #125650, #94224/#94348,
#125351 and #78551.
Fixes#127234
Co-authored-by: auroracapital <info@auroracapital.nl>
Co-authored-by: BrunoBza <189763786+BrunoBza@users.noreply.github.com>
Co-authored-by: gkd2323c <gkd2323c@users.noreply.github.com>
Co-authored-by: Enzo Adami <enzo.eliott.adami@gmail.com>
(cherry picked from commit 5379a417c560e1f669fa2d27e61837b2004e125e)
The replacement no longer depends on any captured group, so drop the
groups from _STRICT_URL_USERINFO_RE and substitute a constant string.
Update the redact_sensitive_text docstring to say the entire userinfo
is masked, and fold the standalone opaque-username test into the
existing parametrize so it also checks redaction stays off by default.
The dispatch guard now uses _COMPRESSION_MARKER_ARTIFACT_RE, so the comment on
_COMPRESSION_MARKER_RE no longer describes its job: only renderer tests use it to
assert a complete first sentence. Say that, and keep the editability reasoning on
the artifact regex alone. The count fragment was typed three times; a single
_MARKER_COUNT_RE keeps the matchers derived rather than re-typed, as the module
docstring promises. No behaviour change.
#128682 changed restart_count to reset whenever a response falls through,
but the apply_retry_restarts docstring still called it a per-turn
accumulator. Align the docstring with the new semantics so the next reader
doesn't reintroduce the turn-wide cap.
Refs #128000
The refunding-restart cap (#106108) counted every redirect for the whole
turn, so a normal interactive turn -- the user answering clarify cards or
sending follow-ups while the model works -- ended as
redirect_restart_limit_exceeded on the (api_max_retries + 1)th mid-turn
message, even though a response completed between each of them.
The cap exists for requests that are cancelled over and over with nothing
completing. Reset restart_count whenever a response reaches processing, so
back-to-back cancelled requests still hit the bound and release the turn
lease, while corrections separated by real progress no longer kill the turn.
Fixes#128000
(cherry picked from commit 3a286a2201ee055b2eb6904523934f60e5e1344c)
The Responses replay kill switch (_codex_reasoning_replay_enabled) was
session state that nothing reset, so a route that tripped it kept replay
off on every route the session moved to afterwards: a /model switch,
fallback activation, or the turn-start restore back to the primary.
The verdict now belongs to the route that earned it: all three route
changes reset it, and the switch rollback snapshot covers it.
Salvage of #61552 (reapplied on current main; the original's
switch_model / restore hunks no longer apply).
An invalid_encrypted_content 400 (or the Codex 401 token_expired twin)
used to strip every cached codex_reasoning_items blob AND turn
replay off for the rest of the agent's life, so a single rotated
sealing key cost all cross-turn reasoning continuity in a long CLI
session or a cached gateway agent.
The first rejection now only strips the stale blobs (persisted messages
and the request copy the retry is rebuilt from); blobs the route mints
afterwards are sealed with its current key and keep replaying. A second
rejection means the route cannot round-trip its own fresh blobs, so
replay is disabled for the session as before: a hostile route costs one
extra 400 compared to main.
The request-copy strip is required: main's global kill switch hid that
the retry was rebuilt from api_messages, which still carried the blob.
Review follow-up. model_preserves_prior_thinking pinned Fable/Mythos to
exactly 5.0/5.1 and returned False for any unparsed Claude id, so the next
minor (claude-fable-5-2, claude-mythos-6, a new family) silently fell back
to the cache-busting strip. Failure directions are asymmetric: replaying a
block the API strips is free, stripping on a model that keeps it rewrites
the cached prefix. Unknown Claude ids now keep; Fable/Mythos keep from 5.0;
Haiku, Claude 3, Opus < 4.5 and Sonnet < 4.6 keep stripping. Non-Claude ids
are unchanged (never keep).
Document in anthropic_thinking_replay why signature-rejection fingerprinting
stays request-wide: the 400's messages.N.content.M path indexes the
converted wire, while recovery only sees pre-conversion api_messages, and
that recovery deliberately covers last-turn-only native models too.
native_anthropic_accounting_projection rebuilt assistant rows from content only, so on
canonical input (preflight, tail walk) a promoted reply with content="" and an
api_content sidecar priced nothing for text that build_api_messages substitutes and
sends. The projection now lets a
non-empty string sidecar win over content, matching the generic wire shadow on main.
Post-build input is unchanged (the sidecar is already in content); ordered turns and
context-selection clones may overcount, never undercount.
The accounting invariant gains an assistant_api_content case that measures replayed
assistant text on the real default assembly: red before (preflight/tail_walk 0 vs
1999), green after.
_native_anthropic_budget read self.api_mode directly while _stale_thinking_on_wire,
called one line earlier in _walk_tail_budget, reads the same route facts as optional
attributes. A compressor built without __init__ (test_compressor_image_tokens) raised
AttributeError in the walk; real instances always set the fields, so this only aligns
the two route predicates.
The compressor's native tail budget read rejected-signature fingerprints
from the session DB only. With persistence disabled (or a failed persist)
the agent's preflight stripped the rejected blocks while the tail walk
still priced them, so the two estimates diverged after a signature
recovery. The agent now binds its session-keyed rejection set onto its
context_compressor (same set object, so later updates reach both), and
the tail walk prefers that in-memory set for its bound session before
falling back to the persisted fingerprints.
#129620 made stale_thinking_reaches_wire() true for preserved-thinking native Claude
(c19252d), so _walk_tail_budget charged the canonical reasoning keys on every turn,
while preflight priced the native projection minus suppressed blocks (643ca56,
9e0ae8d). Measured on a real SessionDB: after a signature rejection the walk charged
243 vs 115 for preflight, and storage-only reasoning rows (e.g. written by another
provider before a model switch) 2033 vs 27, although the wire carried no thinking.
On that route the walk now prices each row through the same projection and the
session's persisted rejection fingerprints. Other routes are unchanged.
Four defects in the durable signature-rejection state that #129620 introduced
(c19252d, JoaoMarcos44, 2026-09-30; reshaped in c18f9e2, 2026-10-01):
- The fingerprint set was cached on the agent with no session key. /new, /resume and
/branch rotate session_id on a live agent, so the next session reused the previous
session's set and never loaded its own persisted rejections; a signature that
session had already been rejected for went back on the wire. The cache is now keyed
by session_id and reloads when it changes.
- Compression publishes the child with the session's initial model_config, so the
persisted fingerprints stayed on the parent while the child's retained tail still
held the rejected rows; resuming the child replayed them. They are now carried in
_carry_session_state_to_child, next to /goal, /heartbeat and /loop.
- A rejection with nothing to fingerprint persisted a "*" strip-all marker, which then
stripped every valid signature the session produced afterwards. That fallback is now
request-local, like the established one-request repair; the legacy dict read path
that could restore it (only ever written by unmerged revisions of #129620) is gone.
- Suppression and recovery were gated on api_mode alone, so Kimi and DeepSeek
Anthropic-compatible routes (and other third-party endpoints) got native persistent
suppression instead of their own replay contract. Both now require the native route
from anthropic_thinking_route().
The readable-mirror lookup uses the converter's carrier precedence.
Canonical history is still never mutated.
native_anthropic_accounting_projection (added in #129620, bd3a75f, JoaoMarcos44,
2026-10-01) re-derived carrier precedence on its own: any ordered block whose type
was replayable made the ordered sidecar authoritative. The converter instead uses the
ordered sidecar only when a block survives _sanitize_replay_block, and otherwise
falls back to reasoning_details. With a dataless redacted_thinking sidecar plus valid
signed reasoning_details, the wire carried the details thinking while accounting
charged none of it (and dropped reasoning_content as if a carrier existed).
Extract assistant_replay_carrier() from _convert_assistant_message and use it for
the projection, so conversion and accounting share one precedence rule.
_manage_thinking_signatures decided kimi / deepseek / third-party / native inline, and
the signature-rejection state added in #129620 needs the same answer. Move the
classification into anthropic_thinking_policy.anthropic_thinking_route() and have the
converter branch on it, so the two cannot drift. No behaviour change for conversion;
the converter's now-unused endpoint imports are dropped.
OPENAI_MODEL_EXECUTION_GUIDANCE told every GPT-family session to use
terminal/execute_code for arithmetic and terminal/read_file/search_files
for hashes, time, system state, files and git, even when the session's
toolset had none of those tools. Lean toolsets then refused plain
arithmetic as "prohibited mental computation".
Drop each guidance line when none of the tools it names are in
valid_tool_names. The toolset is fixed per session, so the system prompt
stays cache-stable.
Fixes#106506
Stamped only on a benched pool, a cooling-down session whose pool was not benched (backoff without a reset, the exhausted-chain cooldown) reloaded auth.json on every turn, where main returned without I/O. Stamping as soon as the interval admits a check caps that at one pool load per probe interval; a check sooner could only replay the probe's cached verdict.
The comment explains the index reset in the not-activated branch, which already carries a two-line version of it; the rate-limit check that used to sit under the long copy moved. The new pool method's docstring named its one caller.
The Codex usage probe caches its verdict per token for five minutes and a
fresh 429 does not clear it. When the endpoint reports quota left while
requests still 429, a fallback-pinned session restored the primary, failed,
and fell back again on every turn of that window, each turn costing a failed
request and two notices.
Lift at most once per probe interval per agent, stamped only when the pool
is actually benched. A genuine reopen is still picked up within one interval,
and benched turns between checks return before touching auth.json again.