Commit Graph

6295 Commits

Author SHA1 Message Date
teknium1
c687e367c9 fix(memory): warn every profile whose memory provider is unavailable; say "Install declined" for a first install
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.
2026-10-02 04:09:57 -07:00
teknium1
a5144c71e9 fix(memory): show every memory-provider migration outcome, ask once per update
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>
2026-10-02 04:09:57 -07:00
funky-xamarin
80c87376db fix(memory): surface startup migration outcomes through warning sink 2026-10-02 04:09:57 -07:00
Ryan Hill
2ce704550b fix(agent): execute local tool batches through per-tool pipeline 2026-10-01 23:50:31 -07:00
teknium1
20e25850a2 Revert "docs(agent): the authorization-gate bound follows the turn's approval window"
This reverts commit 214998ecc0.
2026-10-01 13:23:12 -07:00
John Paul Soliva
803cd03521 fix(git): neutralize repository-named filters in the session snapshot and subagent worktrees
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)
2026-10-02 01:24:41 +05:30
kshitijk4poor
266499524e fix(gateway): deliver and bound-sync generated media in cache/generated
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.
2026-10-02 01:18:03 +05:30
finn763
823385e5cb fix(agent): keep generated images/videos out of swept media caches (Closes #126445)
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)
2026-10-02 01:18:03 +05:30
kshitijk4poor
815b79652d fix(state): keep stored token_count when the agent flush row carries token_count=None
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.
2026-10-02 01:04:12 +05:30
kshitijk4poor
440b97eae0 fix(state): keep stored token_count when a digest-matched replay rewrites its row
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.
2026-10-02 01:04:12 +05:30
kshitijk4poor
d037aba450 fix(state): keep row-addressed resume off the replay digest path
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.
2026-10-02 01:04:12 +05:30
JoaoMarcos44
be20a423ba fix(state): recover unaddressed rewrites by logical identity
(cherry picked from commit c6259114b5551cbeed7051d097142d77048da375)
2026-10-02 01:04:12 +05:30
kshitijk4poor
77c373ac5f refactor(agent): drop the stream watch's redundant pre-trim
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.
2026-10-02 00:53:19 +05:30
kshitijk4poor
acdf435054 refactor(agent): feed both reasoning watches in one short-circuit check
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.
2026-10-02 00:53:19 +05:30
kshitijk4poor
48e3894d0e fix(agent): check a streaming loop every tail window once the reply is long
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.
2026-10-02 00:53:19 +05:30
John Paul Soliva
8ceb1761c4 fix(agent): the stream watch holds a bounded tail and reads both reasoning fields
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)
2026-10-02 00:53:19 +05:30
John Paul Soliva
b6aae42c3b fix(agent): cut a runaway repetition loop while it streams, callback or not
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)
2026-10-02 00:53:19 +05:30
kshitijk4poor
c900b1df58 refactor(agent): simplify strict URL userinfo substitution
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.
2026-10-02 00:52:27 +05:30
Yuan Li
00227371f2 fix(agent): redact strict URL userinfo credentials
(cherry picked from commit 526f376c3ccafcbe44f1398bf5f65926216f95ed)
2026-10-02 00:52:27 +05:30
kshitijk4poor
0e805041d5 refactor(agent): share the marker count fragment and retarget the old regex comment
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.
2026-10-02 00:50:20 +05:30
kshitijk4poor
e2c282c2ed docs(agent): describe restart_count as restarts since the last response
#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
2026-10-02 00:50:20 +05:30
FroRaut
40aa83919e fix(agent): count restarts since the last response toward the redirect cap
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)
2026-10-02 00:50:20 +05:30
JoaoMarcos44
1897982533 fix(agent): block partial compression markers before dispatch
(cherry picked from commit a558bd0aabf8e779fd4e12a2273ba6dda768454f)
2026-10-02 00:50:20 +05:30
dsad
1798b335c4 fix(agent): reasoning replay verdict resets when the route changes
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).
2026-10-01 12:08:13 -07:00
teknium1
9c82eab919 fix(agent): one rejected reasoning blob no longer ends replay for the session
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.
2026-10-01 12:08:13 -07:00
kshitijk4poor
5d077106b8 fix(anthropic): future Claude ids keep prior thinking by default
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.
2026-10-02 00:18:00 +05:30
kshitijk4poor
6a90232769 fix(anthropic): native accounting charges the assistant api_content sidecar
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.
2026-10-02 00:18:00 +05:30
kshitijk4poor
127735f505 fix(compression): native tail budget reads route facts like its sibling predicate
_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.
2026-10-02 00:18:00 +05:30
kshitijk4poor
75e9105bb4 fix(compression): tail walk sees in-memory rejected thinking without a DB
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.
2026-10-02 00:18:00 +05:30
kshitijk4poor
e8cc8c31c4 fix(compression): tail walk prices native preserved thinking like preflight
#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.
2026-10-02 00:18:00 +05:30
kshitijk4poor
c29704d1a6 fix(anthropic): keep rejected-thinking state scoped to its session and route
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.
2026-10-02 00:18:00 +05:30
kshitijk4poor
8c11a233e2 fix(anthropic): price the carrier the converter actually replays
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.
2026-10-02 00:18:00 +05:30
kshitijk4poor
c04e16f488 refactor(anthropic): one thinking-replay route classifier
_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.
2026-10-02 00:18:00 +05:30
JoaoMarcos44
06bdebd04c fix(anthropic): align accounting with native wire 2026-10-02 00:18:00 +05:30
JoaoMarcos44
6403dd325c fix(anthropic): simplify recovery and make accounting explicit 2026-10-02 00:18:00 +05:30
JoaoMarcos44
3cacbdf677 fix(anthropic): invalidate usage anchor on signature repair 2026-10-02 00:18:00 +05:30
JoaoMarcos44
7cd66f7188 style(anthropic): separate replay helpers 2026-10-02 00:18:00 +05:30
JoaoMarcos44
1fd41d8770 fix(anthropic): exclude rejected thinking from preflight 2026-10-02 00:18:00 +05:30
JoaoMarcos44
ae422f1108 fix(anthropic): suppress rejected readable mirrors 2026-10-02 00:18:00 +05:30
JoaoMarcos44
8b1cd16511 style(anthropic): keep helper module formatting 2026-10-02 00:18:00 +05:30
JoaoMarcos44
ec26994742 fix(anthropic): align canonical preflight accounting 2026-10-02 00:18:00 +05:30
JoaoMarcos44
bdcfdfcfa1 fix(anthropic): dedupe selected-history accounting 2026-10-02 00:18:00 +05:30
JoaoMarcos44
0302bb98f9 fix(anthropic): share exact-once thinking projection 2026-10-02 00:18:00 +05:30
JoaoMarcos44
6e9789ffd5 fix(anthropic): suppress after selection and price replayed thinking 2026-10-02 00:18:00 +05:30
JoaoMarcos44
17e4143aee fix(anthropic): distinguish snapshot dates from minor versions 2026-10-02 00:18:00 +05:30
joaomarcos
582750e3f9 fix(anthropic): make preserved-thinking replay coherent 2026-10-02 00:18:00 +05:30
chelsealong
41201bf215 fix(agent): gate execution-guidance tool lines on the session's toolset
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
2026-10-01 11:45:41 -07:00
kshitijk4poor
105e876586 perf(fallback): stamp the early-reopen throttle before loading the pool
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.
2026-10-02 00:03:01 +05:30
kshitijk4poor
74449250c7 docs(fallback): fold the stranded #20465 comment into its branch; keep the pool docstring caller-free
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.
2026-10-02 00:03:01 +05:30
kshitijk4poor
a3512a527f fix(fallback): one early-reopen restore per probe interval, not one per turn
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.
2026-10-02 00:03:01 +05:30