Commit Graph

919 Commits

Author SHA1 Message Date
kshitij 4ce0d64be4 test(caching): pin signal precedence and the openrouter-host opt-out
Review follow-up. Three coverage gaps in the LiteLLM matrix:

- The operator opt-out on a litellm-named provider pointed at an OpenRouter
  host. That route previously took the OpenRouter branch and ignored an
  explicit per-model `prompt_caching: false`; it is the only cell in the
  differential matrix where the salvage REMOVES caching, so pin it as
  intended rather than leaving it to be read as a regression.
- Signal precedence: an explicitly litellm-named provider grants even on a
  lookalike host, because the provider id is an independent signal and only
  the host-derived signal is token-gated. Intentional, now documented.
- A hyphen-delimited host label (`my-litellm-gw.internal.example.com`),
  which the token matcher handles but nothing exercised.

Traded the redundant `claude-3-7-sonnet` parametrize cell for the new host
case, so the matrix covers more shapes with the same cell count.

Tests: 83 passed. All three production fixes re-mutation-checked against
the final stack.
2026-08-16 14:34:30 +05:30
kshitij 435d6f30b5 fix(caching): match the litellm provider id token-wise too
Self-review follow-up. The previous commit fixed substring matching on the
HOST but left the provider-id side as a bare substring, so a user-named
provider like `custom:notlitellm` or `mylitellmthing` still matched and was
handed Anthropic markers — the same bug class, half-fixed.

Both signals now match `litellm` as a whole delimited token via a shared
helper. Real spellings (`litellm`, `custom:litellm`, `litellm-router`, and
the already-lowercased `LiteLLM`) still match; lookalikes no longer do.

Tests: 71 passed. Adds lookalike-provider and real-spelling guards; both
new guards mutation-checked. Differential matrix over 2688 configs vs
origin/main: 60 changes, every one a Claude model on a genuine LiteLLM
route getting the envelope layout, zero pre-existing routes altered.
2026-08-16 14:34:30 +05:30
kshitij 1b0e953b47 fix(caching): use the envelope layout for LiteLLM Claude on the OpenAI wire
Follow-up to the salvaged LiteLLM cache grant. The grant itself is right;
four things about how it was scoped were not.

1. Layout. The branch returned the native inner-block layout
   (use_native_layout=True) on api_mode == "chat_completions". That layout
   writes a TOP-LEVEL msg["cache_control"] on role:tool and empty-content
   messages and depends on the Anthropic adapter to relocate it into the
   block — but that adapter only runs for api_mode == "anthropic_messages"
   (agent/transports/anthropic.py registers there), and the
   chat_completions transport does no relocation. Measured on a 3-tool-turn
   transcript: 2 of the 4 available breakpoints landed on markers the
   provider never sees. Worse, when LiteLLM itself relocates a top-level
   marker for an OpenRouter-backed Claude route
   (OpenrouterConfig._move_cache_control_to_content), the marker lands on
   an empty assistant turn and produces a cache_control-marked empty text
   block — the HTTP 400 "text content blocks must contain" shape already
   guarded in agent/anthropic_adapter.py (#69512). Switched to the envelope
   layout, matching every other OpenAI-wire grant in this function:
   4 of 4 breakpoints honored, zero empty blocks.

2. Host matching. `"litellm" in base_url_hostname(...)` is the substring
   false-positive class base_url_hostname's own docstring warns against; it
   granted Anthropic markers to notlitellm.example.com,
   foolitellmbar.example and friends. Replaced with a label-token match in
   a named helper, so "litellm" must be a whole dot- or hyphen-delimited
   token. All three of the original test hosts still match; a "litellm"
   path segment on an unrelated host still does not.

3. Transport gate. `not is_anthropic_wire` also swept in codex_responses,
   bedrock_converse and codex_app_server. Gated on
   api_mode == "chat_completions" explicitly.

4. Operator override. The grant is inferred from a provider/host name, but
   the custom-provider capability lookup was gated on is_anthropic_wire, so
   an explicit `prompt_caching: false` for the route+model was honored on
   /v1/messages and silently ignored on /v1/chat/completions. The lookup
   now also runs for a LiteLLM route, and its layout follows the transport
   rather than the declaration (an explicit `true` must not promote a
   chat_completions request to the native layout).

Tests: 64 passed. Adds the wire-shape contract the original matrix was
missing (asserts no breakpoint sits on the message envelope, rather than
only checking the returned tuple), plus lookalike-host, other-transport,
and both operator-override directions. All five guards mutation-checked —
reverting each fix turns the corresponding test red.
2026-08-16 14:34:30 +05:30
Justin Bowes ff4df5e54d fix(caching): engage prompt caching for LiteLLM Claude on the OpenAI wire
anthropic_prompt_cache_policy() only granted Anthropic cache_control
markers to LiteLLM over the native Anthropic wire
(api_mode == "anthropic_messages"). A LiteLLM deployment exposing the
OpenAI-compatible surface instead (/v1/chat/completions, /v1/messages
-> 404) matched no grant branch and fell through to (False, False): no
cache_control injected, the system prompt sent as a plain string, and
the provider serving zero cache hits -- the entire prompt re-billed at
full price on every turn. Silent: no error, no warning, usage simply
shows 100% uncached input forever.

Add one branch after the is_anthropic_wire/is_claude case that grants
caching to Claude-family models on a LiteLLM endpoint regardless of
wire, with the native inner-block layout. Same failure class already
documented in-function for Qwen/DashScope.

Design:
- Gated on the Claude family only (is_claude); a Gemini/GPT/Qwen route
  through the same proxy must not receive markers (they may reject the
  cache_control block format -- cf. the DeepSeek/OpenCode exclusion).
- Matches on provider string OR base_url host, since provider naming
  varies per install (litellm, custom:litellm, or a bare custom alias
  pointed at a LiteLLM host).
- prompt_caching.cache_ttl: false still wins (the _cache_disabled early
  return is untouched).
- Generic strict OpenAI-wire custom providers (e.g. Fireworks) remain
  excluded -- verified by the existing over-reach regression test.

Tests: adds TestLiteLLMOpenAIWire covering the grant (several model
spellings x provider/host signals), no-over-reach (non-Claude on the
same proxy get nothing; operator disable wins), and adjacent behavior
(LiteLLM in Anthropic proxy mode still native layout). Full module:
43 passed.

Closes #84506. Original diagnosis, patch design, and measurements by
@ottosulin.
2026-08-16 14:34:30 +05:30
fangliquanflq 8fc4189edd fix(agent): harden canonical tool call deduplication 2026-08-16 01:59:08 -07:00
fangliquanflq a55d29d9fb fix(agent): canonicalize duplicate tool call arguments 2026-08-16 01:59:08 -07:00
Martin Koistinen da392043a9 fix(agent): include timezone and UTC offset in system prompt timestamp
The "Conversation started:" line carried a bare date (%A, %B %d, %Y). Tools
that accept instants -- nutrition, calendar and similar MCP servers -- reject
naive datetimes and require an explicit UTC offset, so the model had to infer
EST vs EDT from the date alone. Near a DST boundary that is a coin flip, and a
wrong guess does not error: it silently writes the record onto the wrong day.

Append the IANA zone (when configured), the zone abbreviation and the UTC
offset, e.g.:

  Conversation started: Saturday, August 15, 2026 (America/New_York, EDT, UTC-04:00)

get_timezone() returns None when no timezone is configured; in that case the
line falls back to the abbreviation and offset of the server-local (still
tz-aware) time, so behaviour is unchanged for users who never set one:

  Conversation started: Saturday, August 15, 2026 (EDT, UTC-04:00)

Daily byte-stability is preserved -- the property the date-only format exists
to protect (PR #20451). Zone name, abbreviation and offset are all constant for
the whole day; they shift only at a DST transition, where a change is correct.
The static-prefix reconstruction guard in _restore_plugin_sections matches on
"\n\nConversation started:" and is unaffected by a suffix after the date.

test_datetime_is_date_only_not_minute_precision used `re.search(r":\d{2}")`
over the whole line as a proxy for "no time-of-day". A UTC offset also matches
that pattern, so the check now applies to the date portion (everything before
the zone parenthetical) and the invariant is tightened rather than relaxed:

- test_datetime_includes_utc_offset asserts the offset is present
- test_datetime_line_is_stable_across_rebuilds asserts two rebuilds in the
  same day produce a byte-identical line

Fixes #87403

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-16 01:57:59 -07:00
webtecnica b48ab1b4ad fix(agent+discord): guard truncated-response continuation loops and cap Discord split delivery (#86581) 2026-08-16 01:55:51 -07:00
fangliquanflq 0fd059e745 fix(agent): preserve stalled-provider escalation 2026-08-16 01:55:00 -07:00
Teknium 20fcc11a0c refactor(hooks): share fail-closed approve logic; update sibling tests
Follow-up to the #28953 salvage:

- Extract _resolve_block_from_details() so resolve_pre_tool_block and
  _dispatch_pre_tool_call_hooks share ONE fail-closed approval-gate
  implementation. This also gives the new dispatcher the observability
  context wrapping around request_tool_approval that the original PR's
  inlined copy lacked.
- Update sibling tests that patched resolve_pre_tool_block at the three
  migrated dispatch sites to patch _dispatch_pre_tool_call_hooks with the
  (block_message, modified_args) tuple contract.

Verified: 448 targeted tests green; E2E with a real shell hook in an
isolated HERMES_HOME rewrote a live write_file call (path + content)
through handle_function_call, with block and negative paths intact.
2026-08-15 23:03:52 -07:00
Teknium 61f2738205 test(tool-executor): close the second race — deadline vs middleware preamble
The first hardening (deterministic worker start) still went red on its
own PR's CI: the 0.1s deadline can ALSO expire between worker start and
handle_function_call — argument parsing, hooks, and cold imports run in
that window, and the timeout interrupt then returns the middleware
without dispatching (first_started unset; observed 0.15s tool error vs
0.1s budget in the slice-4 log).

Deadline raised to a 1.0s floor: still fast, ~7x the worst observed
preamble, and the submit() sync from the first commit keeps the
countdown anchored to worker start. The clarify human-wait test's sleep
rises to 1.3s so it still outlasts the deadline (the relationship the
test exists to pin). Sabotage v2 injects BOTH races (0.3s thread start
+ 0.5s preamble): hardened passes, un-hardened reproduces the exact CI
failure.
2026-08-15 18:15:57 -07:00
Teknium d24d294421 test(tool-executor): deterministic worker start kills sequential-timeout flake
test_sequential_tool_timeout_emits_result_and_continues failed twice in
two days on unrelated computer_use PRs (slice 4, xdist): assert
first_started.is_set() -> False. Root cause: the sequential timeout path
computes deadline = now + timeout_s right after executor.submit(); with
the test's 0.1s deadline, a loaded CI worker can take longer than the
whole deadline just to START the pool thread, so the future is cancelled
before the tool ever dispatches.

Fix: an autouse fixture subclasses DaemonThreadPoolExecutor so submit()
blocks (bounded 10s) until the worker callable has begun — the deadline
now races the tool, not the thread scheduler, which is what these tests
mean to pin. Timeouts stay tight (0.05s), so the suite stays fast.

Sabotage-proven: a 0.3s injected thread-start delay reproduces the exact
CI failure without the fixture and passes with it.
2026-08-15 18:15:57 -07:00
fangliquanflq 79b7d969d3 fix(gateway): reject unstamped durable ordinal rewinds 2026-08-15 13:36:25 -07:00
PRATHAMESH75 ece3bc7d52 fix(agent): skip the automatic background review inside delegation subagents
The post-turn background review fork (`agent/background_review.py`) inherits
the parent agent's live runtime by default. That is a cost win when the parent
IS the main chat model (warm prompt cache, cheap), but the fork also fires
inside delegation subagents, where it inherits the *subagent's* model. When a
subagent runs a premium delegation model, the review silently replays the whole
conversation and emits skill/memory-update output at premium rates, with
nothing in the log or config flagging it (#85859).

Subagents are already barred from writing shared MEMORY.md
(`DELEGATE_BLOCKED_TOOLS`) and are spawned with `skip_memory=True`, so an
automatic review here has little to persist. Guard `_spawn_background_review`
(the single choke point both the turn-finalizer and codex-runtime callers pass
through) to return early when `_delegate_depth > 0`. An explicit `/refine`
(`focus` set) is a deliberate user request and still runs; the top-level path
is unchanged.

Fixes #85859
2026-08-15 02:44:38 -07:00
coe0718 7f7aefe5cb fix: restore complete message timestamp coverage 2026-08-15 01:04:19 -07:00
spfcraze b9f7525a1b test(agent): pin measured-work regression for display-flag config cache
(cherry picked from commit 57a7044c20965150e74b0ecadb2550f0ed81ee8e)
2026-08-15 00:36:03 -07:00
spfcraze e25cafc83e perf(agent): cache per-turn display-flag config reads
_file_mutation_verifier_enabled and _turn_completion_explainer_enabled
re-read config.yaml on every call via load_config() (~1ms deepcopy per
call). finalize_turn runs these gates at the end of every turn, so each
turn paid two redundant config deepcopies. The sibling
_credits_notices_enabled already caches on self; mirror that pattern.

The env-var override stays authoritative and uncached, so runtime flips
still work. Config flips now apply on the next session, matching the
documented sibling semantics.

(cherry picked from commit f2d0e00d71ba35fa3a53cd41076261e3c5feb45d)
2026-08-15 00:36:03 -07:00
blunkjamie-dev 163d7af310 fix(session-search): forward detail through agent paths
(cherry picked from commit 5f6de984f170ef470c7fbbd7662484bfaffc821d)
2026-08-15 00:34:29 -07:00
Teknium 0cbc4ce83b fix(streaming): gate the interrupt worker join on live Relay managed execution
The unconditional 2s join before InterruptedError delayed interrupt
detection when Relay managed execution was not active (CI:
tests/run_agent/test_interrupt_propagation.py — detection took 2.34s
against a <1.0s budget, because the mocked worker sleeps 5s and there
is no Relay scope to unwind).

Extract the join into _join_worker_for_relay_teardown(), which no-ops
unless a Relay runtime exists AND managed execution consumers are
registered — the only case where an orphaned physical scope can corrupt
the LIFO stack (#81521). Applied at all three interrupt sites
(streaming, non-streaming, Bedrock streaming). The regression test now
simulates a live runtime so the join path stays covered.
2026-08-14 22:11:30 -07:00
HexLab98 81c4f3a143 test(streaming): cover interrupt join, orphan drain, and EIO paint freeze 2026-08-14 22:11:30 -07:00
kshitij 24573b396b refactor: bind gateway anti-growth guard to locals, trim overlong comment
Simplify-code pass: gateway/run.py called estimate_messages_tokens_rough
6x on the same data in the anti-growth guard (condition + warning f-string).
Bind to _hyg_in_toks/_hyg_out_toks locals like the conversation_compression.py
guard already does. Also trim the comment from 10 lines to 4 (keep the WHY,
drop the WHAT) and remove an extra blank line before TestCompactedTurnsStaySearchable.
2026-08-15 10:22:47 +05:30
dhruv kejriwal ffaa63f887 fix(compressor): never commit a compression that grows the transcript (in-place path)
The gateway rotation guard (#83339) only protects the rotate path, but
in-place compaction commits inside compress_context() via
archive_and_compact — before the gateway can inspect the result. Add the
anti-growth check at the commit site so both paths are covered: a
compression whose rough output exceeds its input is a strict no-op
(original transcript kept durable, session identity untouched).

Covers the observed failure where session hygiene persisted 426 -> 426
messages and ~379K -> ~688K tokens.
2026-08-15 10:22:47 +05:30
joaomarcos 39e480c051 fix(state): close leaked SessionDB connections on exception paths (#83226)
SessionDB could leave native SQLite handles open when construction failed
partway through schema/pragma/FTS/repair/lock/interrupt handling. Other
short-lived callers (MCP reads/polling, session search, reactions, trace
upload, insights, shutdown recovery) opened temporary SessionDB handles
without a complete ownership boundary. API-server profile caches and
RetainDB shutdown had similar late-close races. Under sustained load this
exhausted file descriptors (EMFILE).

- Close partially initialized SessionDB connections on every constructor
  exception path via a finally block guarded by an initialization-complete
  flag.
- Close temporary/cross-profile SessionDB handles in finally blocks across
  CLI, MCP, search, trace, reactions, insights, and recovery paths.
- Add API-server per-profile cache ownership and disconnect cleanup.
- Make RetainDB writer-queue shutdown exception-safe: track connections per
  thread, close on worker exit, reject new enqueues after shutdown starts,
  and sweep any connections left by short-lived threads.
- Add regression coverage for constructor failures, worker-thread readers,
  API disconnect failures, shutdown recovery, RetainDB late enqueue, and
  foreign-loop async clients.

Salvage notes: the original PR's per-thread WAL-reader ownership changes
were superseded by main's read-connection pool (permits + checkout/return);
its cron timeout-abandon fix is credited separately to #72822's earlier
identical fix.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-14 21:41:26 -07:00
Teknium 8dc5608a78 fix(compression): adopt live continuation tip at flush across multi-hop chains
A turn writing against a session already closed by compression died with
session_persistence_failed and a misleading "this is often a full disk"
dialog, even though the store was healthy and a live continuation existed
(#82001). Depth-1 recovery (find_live_compression_child) could not resolve
lineages with >=2 compression hops (root -> mid -> tip), reproduced
independently on two- and three-hop chains.

- run_agent.py flush chokepoint: on CompressionSessionClosedError, resolve
  tip = db.get_compression_tip(old_id) (canonical bounded transitive walk),
  adopt only when tip != old_id AND the tip row is live, retry the flush
  exactly once (adoption budget); otherwise fail closed.
- gateway/session.py append_to_transcript: replace the depth-1 live-child
  lookup with the same tip + liveness contract, so gateway transcript
  reroutes follow full chains.
- agent/conversation_compression.py _adopt_live_compression_child: turn-start
  recovery preflight now resolves via get_compression_tip with the same
  liveness check, closing the last depth-1 consumer in this family.
- classify_persistence_error: new "compression_closed" bucket; the turn-end
  explanation names compression rotation and tells the client to refresh the
  session id instead of blaming a full disk.

Tests: depth-1 adoption, multi-hop chain adoption (agent + gateway), fail
closed with no continuation / stale-closed (ws_orphan_reap) tip, exactly-once
adoption budget, and error-wording guards (compression-closed never mentions
disk; real disk failures keep disk guidance).

Closes #82001

Co-authored-by: Al3xand3r1987 <125030427+Al3xand3r1987@users.noreply.github.com>
Co-authored-by: yuzilongleif-collab <235949691+yuzilongleif-collab@users.noreply.github.com>
2026-08-14 21:39:44 -07:00
burak33bb 453e6d8b95 fix(moa): preserve facade across client rebuilds 2026-08-14 21:36:41 -07:00
webtecnica f316f7d086 fix(session): drop empty tool_calls in repair_message_sequence (#77921) 2026-08-14 21:25:48 -07:00
liuhao1024 b2453b5894 fix(sanitize): drop tool_calls key when dedup removes all calls
The dedup pass in sanitize_api_messages (introduced by #58327) can
produce an empty tool_calls array when all tool_call_ids in a message
are duplicates of earlier messages in a long conversation history.

DeepSeek v4 and newer OpenAI reject empty tool_calls with HTTP 400:
'Invalid messages[N].tool_calls: empty array'.

When kept_tcs is empty after dedup, drop the tool_calls key entirely
instead of writing tool_calls: [].

Fixes #64335
2026-08-14 21:25:48 -07:00
Ufonik 54aad0d670 fix(tools): refresh activity heartbeat while a tool call is in flight (#84491)
The gateway turn-inactivity watchdog (gateway/run.py::_watch_gateway_turn_inactivity)
abandons a turn once seconds_since_activity exceeds the inactivity timeout
(default 30 min). Activity was only stamped when a tool started and when it
completed, so a tool call that runs silently for 30+ minutes (quiet builds,
long pytest suites, large downloads, network waits with no output) froze the
clock and the watchdog hard-abandoned a turn that was still making progress,
reaping the tool's processes mid-execution (issue #84491).

Add a daemon-thread heartbeat inside _run_agent_tool_execution_middleware that
touches agent._touch_activity every 30s while the tool is in flight, until the
call returns. Both the sequential and concurrent execution paths funnel through
this single middleware, so one heartbeat covers every tool. The thread is
stopped in a try/finally so it always tears down even if execute() raises, and
is never started when a guardrail/authorization block short-circuits before
dispatch. A genuinely hung tool remains bounded by the tool layer's own
timeouts (terminal default 180s, concurrent batch deadline ~420s), so the
heartbeat only extends the turn's life while the call is legitimately running.

Verified by an independent reviewer (no security/logic defects); 5 unit/integration
tests pass on Python 3.12 (upstream CI). The 30-min gateway backstop remains
for turns whose agent loop itself stalls.
2026-08-14 21:20:03 -07:00
HexLab98 c5788b5ea3 test(agent): cover inline hard timeout and in-flight pool-request abort
Pin that cron/subagent non-streaming calls receive a read timeout matching the stale budget, that an explicit timeout is left alone, and that force_close_tcp_sockets finds sockets on httpcore PoolRequest.connection and clears the socket timeout before shutdown without close().
2026-08-14 21:19:55 -07:00
Teknium 5d9e4aaaf2 fix: raise ProviderStreamError for choiceless error chunks + regression tests 2026-08-14 20:46:42 -07:00
beiyesi b55dd04711 fix(streaming): adapt provider errors to relay 2026-08-14 16:40:02 -07:00
beiyesi 04bc5321c9 fix: preserve non-JSON provider stream errors 2026-08-14 16:40:02 -07:00
beiyesi 5cbc645d09 fix: require terminal signal for streamed errors 2026-08-14 16:40:02 -07:00
beiyesi 0fcebb29f4 fix streaming bare data error payloads 2026-08-14 16:40:02 -07:00
beiyesi 661a4c4f88 fix streaming provider error events 2026-08-14 16:40:02 -07:00
kshitij c925b523a1 test: simulate contended wait for resolve+reload path
Two tests asserted resolve+reload events but acquired the lease
immediately (no wait). After gating the reload behind _lease_waited,
these tests must simulate a contended wait via on_wait(0.0) to
exercise the resolve+reload path.
2026-08-15 03:22:16 +05:30
fangliquanflq 6b25e67047 fix(sessions): scope refresh interrupts to active turns 2026-08-15 03:22:16 +05:30
fangliquanflq 967391cd4b fix(sessions): stop lease refresher at turn boundary 2026-08-15 03:22:16 +05:30
fangliquanflq 19b1204392 fix(sessions): revive uncontested expired turn leases 2026-08-15 03:22:16 +05:30
fangliquan f1025b2c00 fix(sessions): fence transcript writes with the turn-lease holder
Refresh-loss interrupt is cooperative, so a stalled writer could still flush after another process reclaimed the conversation. Carry the holder into append_message / append_messages_batch and reject the write in the same SQLite transaction when the lease row is missing, expired, or owned by someone else.
2026-08-15 03:22:16 +05:30
fangliquan c21efeeb52 fix(sessions): keep turn lease across inherited-marker compressions
Presence-only _delegate_from/_branched_from checks stopped the lease walk on
continuations that copied a delegate's model_config, so the first refresh
after rotation missed the parent-key lease and hard-interrupted. A failed
get_session probe also skipped acquire entirely. Walk the lineage inside
the write transaction and treat a probe error as contended, not a fresh
session.
2026-08-15 03:22:16 +05:30
fangliquan 5e2be43fd4 fix(sessions): harden cross-process turn lease wait and refresh
Honor interrupts while waiting for admission, stop the turn when refresh
loses the lease, poll once per second under contention, and test dead-PID
reclaim.
2026-08-15 03:22:16 +05:30
fangliquan 3b09456019 fix(sessions): surface wait status for cross-process turn leases
Emit lifecycle notices while waiting on another process, and return a
resend-friendly timeout result instead of a bare TimeoutError.
2026-08-15 03:22:16 +05:30
fangliquan 6e929a9694 fix(sessions): serialize turns across processes 2026-08-15 03:22:16 +05:30
fangliquanflq 61645cde82 fix(agent): exempt clarify from sequential tool deadline
Clarify waits on a human for up to 3600s or unlimited. The generic sequential timeout was aborting that wait at 420s and leaving the prompt and worker active.
2026-08-15 02:21:39 +05:30
fangliquan 82a1b5a115 fix(agent): suppress late timeout observer events 2026-08-15 02:21:39 +05:30
fangliquanflq ededa8c4f1 fix(agent): bound sequential tool calls 2026-08-15 02:21:39 +05:30
joaomarcos 11c5aae104 fix(compaction): gate checkpoint replay/prune on current request eligibility
A captured native-compaction checkpoint lives in the persisted
codex_reasoning_items sidecar, but the wire restructure that follows it
(prune_pre_checkpoint_items) ran unconditionally: the native gate only
decided whether context_management went into the request, and no signal
from it ever reached _chat_messages_to_responses_input.

So a single checkpoint kept deleting every pre-checkpoint item from all
later requests — after a mid-session swap out of the gpt-5.6 family,
after compression.enabled: false, after the rejection kill switch, and
after a session resume that reloads the sidecar from state.db. The model
receiving the opaque blob was no longer the one able to decode it, and
nothing was logged.

Thread a single native_compaction_eligible boolean, derived from the same
value that gates the context_management field, into the converter. When
ineligible: do not replay type: "compaction" items and do not prune. Safe
because native compaction never truncates Hermes' local history, so the
fallback still carries the full conversation.

All Responses call sites are covered: build_kwargs and convert_messages
derive the flag via _native_compaction_active, the auxiliary/compression
client is explicitly ineligible, and the converter defaults to False
(pre-feature wire) so future call sites are safe by construction.

Fixes #85914

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 10:39:25 -07:00
Teknium fbec3c78fd fix(compression): clear preflight block on provider-confirmed rearm
A provider-confirmed rearm (#85846) resets the shared attempt budget, but
an earlier insufficient-progress verdict left _preflight_compression_blocked
armed, keeping the pre-API gate dark for the rest of the turn — a later
pressure spike could still grow unchecked until the provider overflow
handler fired. Clear the blocker and the stale pressure reading inside the
provider-confirmed rearm branch: the prompt is proven back below the
threshold, so the old verdict describes a request shape that no longer
exists.

Builds on @h-mascot's #84995, whose commit is preserved on this branch;
his rearm condition was superseded by #85846's latch-verified variant, but
the blocker-clear half was correct and is kept.
2026-08-13 23:43:54 -07:00
Henry Mascot 2b9da1f252 fix(compression): re-arm same-turn attempt budget 2026-08-13 23:43:54 -07:00