Commit graph

78 commits

Author SHA1 Message Date
Teknium
1d1b670cb5 fix(compression): reset blocked-overflow dedup on every compression path + noise-filter survival pins
Follow-up fixes for the #62625 salvage:

- Dedup-reset gap (sweeper review): when the block clears while the
  context is STILL over threshold, execution enters the compression
  branch — the PR's 'else' reset never ran, so the warning stayed
  suppressed forever after the first block. _clear_context_overflow_warn()
  now fires on every automatic compression path: turn-context preflight,
  conversation_loop pre-API gate, and the post-tool loop-compaction gate.
- should_compress_info on current main: main refactored should_compress
  into _automatic_compression_blocked()/_locally(); the tuple variant now
  derives its reason from the same in-memory state via
  _compression_block_reason(), keeping cooldown:<s>/ineffective shapes.
- ContextEngine.should_compress_info ABC default now actually returns
  (should_compress(tokens), None) — the PR's default had a docstring but
  no return (returned None, would crash tuple-unpacking call sites).
- Below-threshold guard: the turn-context persisted-cooldown branch and
  the conversation_loop pre-API cooldown branch no longer warn when the
  estimate is under threshold (should_compress_info returns a None
  reason; the preflight pre-check is not a threshold guarantee). The
  pre-API guard also honors compression.max_attempts instead of a
  hardcoded 3, and no longer fabricates a cooldown reason.
- Noise-filter survival (#69550 composition): warning text is now a
  template constant (CONTEXT_OVERFLOW_BLOCKED_WARNING_TEMPLATE) marked
  FAILURE-CLASS, pinned un-swallowed in VISIBLE_COMPRESSION_MESSAGES and
  in new tests that execute the real _TELEGRAM_NOISY_STATUS_RE +
  _prepare_gateway_status_message.
- Contributor mapping for stanislav@local -> sl4m3.
2026-07-23 08:43:21 -07:00
Ethan
e8000b42e7 fix: prevent stale lock-skip signal leaking between compress_context calls
Advisor review found a critical stale-signal leak: if auto-compress
sets _compression_skipped_due_to_lock during a lock-skip, a subsequent
successful manual /compress will see the stale signal, falsely report
'Compression already in progress', and discard the compression results.

Fix:
- compress_context clears _compression_skipped_due_to_lock = None at
  entry so each call's outcome alone determines the signal.
- Unified gateway 'holder: unknown' drift to match CLI/TUI pattern
  (omit holder clause when not a descriptive string).
- Added MagicMock opt-outs in 3 sibling test files broken by the new
  signal check (test_compress_here, test_compress_focus,
  test_compress_plugin_engine).
- Added stale-signal-leak invariant test proving the fix.
2026-07-23 08:19:14 -07:00
Ethan
b86367e496 fix: signal lock-hold to callers when compression skips 2026-07-23 08:19:14 -07:00
Teknium
ec5835ab8b
fix(compression): persist anti-thrash state across process restarts (#69872)
The anti-thrash guard (_ineffective_compression_count) was in-memory
only: a fresh compressor bound to a resumed, already-compacted session
started with compression_count=0 and a disarmed guard, so a
near-threshold session could legally re-compact once per process
restart, forever.

Persist the counter through the durable session-state channel,
mirroring the failure-cooldown (#54465) and fallback-streak (af7dceaf7)
pattern:

- hermes_state.py: sessions.compression_ineffective_count column
  (declarative reconciliation adds it on existing DBs) +
  get/set_compression_ineffective_count accessors.
- context_compressor.py: every strike/clear verdict routes through
  _record_ineffective_compression_verdict() which writes through to the
  session row (no-change verdicts skip the DB write);
  bind_session_state() loads the persisted value; the compression
  rotation boundary carries the counter onto the child row;
  update_model()'s reset also clears the durable copy; the
  ineffective-only fast path in _automatic_compression_blocked() is
  removed because the counter is now durable and another agent's clear
  must unblock a stale local snapshot.
- conversation_compression.py: _refresh_persisted_compression_guards
  re-reads the counter alongside cooldown + fallback streak.

Reset semantics are unchanged: any real provider reading below the
threshold still clears the counter — and now clears it durably too.

Resolves the residual gap identified in #54923 by @lanyusea (the
second-threshold mechanism was superseded by persisting the existing
guard state).

Co-authored-by: lanyusea <lanyusea@gmail.com>
2026-07-23 08:08:48 -07:00
kshitij
ca9c30c7f0 fix(gateway): bound hygiene compression failures 2026-07-23 07:26:27 -07:00
Stephen Schoettler
4035d70bbe fix(context-engine): honor quiet compaction status 2026-07-23 07:26:15 -07:00
Teknium
06a2d77372 fix(compression): gate todo-snapshot merge on real-user tails, refresh stale snapshots
Follow-up hardening on the salvaged merge-into-trailing-turn fix:

- Merge only into REAL user tails (_is_real_user_message probe). Merging
  into scaffolding tails (continuation marker, summary-as-user handoff)
  would upgrade them to real-user evidence after SessionDB projection
  strips the flags, breaking zero-user provenance (#69292 -
  _is_synthetic_compression_user_turn keys on the TODO_INJECTION_HEADER
  content marker, which merge-at-tail would bury mid-content).
- Strip a previously merged snapshot block before re-injection so
  repeated boundaries refresh rather than accumulate todo state, and
  refresh a bare stale snapshot row in place instead of stacking a
  duplicate (empty/stale-skip semantics from #26981 by @YLChen-007).
- Scaffolding tails keep the flagged standalone append (pre-#53890
  status quo; adjacent user rows are repaired downstream by
  repair_message_sequence / _merge_consecutive_roles).
2026-07-23 07:25:44 -07:00
Yingliang Zhang
d2bb6cc251 fix(compression): merge todo snapshot into trailing user msg to avoid consecutive user/user turns
After context compression, the preserved todo list was unconditionally
appended as a standalone user message. When the compressed transcript
already ends with a user message (common case), this creates consecutive
user/user turns — a role-alternation violation some providers reject.

Fix: fold the snapshot into the trailing user message (blank-line separated)
when one exists with plain-string content. Falls back to append when the
tail is non-user, empty, or has structured (list) content.

Rebased on current upstream/main.

Closes #53890
2026-07-23 07:25:44 -07:00
Brett Bonner
17b3a4bd41 fix(compression): preserve flush baseline after abort 2026-07-23 07:25:21 -07:00
Teknium
9981242f88 fix(gateway): widen compression noise filter to all routine status lines
Post-sweep audit of every compression status emission (conversation_loop,
turn_context, conversation_compression): the buffered overflow/attempt-cap
retry chatter (🗜️ 'Context too large…', 'Compressed X → Y, retrying…',
'Context reduced to…'), the #69332-reworded auto-lower notice
("Auto-lowered this session's threshold…"), the aux-provider-unavailable
notice, and the concurrent-compression skip all leaked past
_TELEGRAM_NOISY_STATUS_RE on chat platforms. Add anchored alternatives for
each; the ', retrying'/'— compressing' anchors keep manual /compress
feedback ('Compressed: 30 → 12 messages') and failure/abort notices
visible per the deliberate carve-outs.

Also extract every routine compression status string into importable
template constants in agent/conversation_compression.py (single source of
truth shared by all emission sites), so tests can iterate the actual
emitted wording instead of hand-copied literals.
2026-07-22 17:14:36 -07:00
Yingliang Zhang
8c745314b9 fix(desktop): synchronize context usage and compaction status 2026-07-22 16:58:58 -07:00
Gabriele Di Gesù
356ff99030 feat: log compression attempt telemetry 2026-07-22 08:13:41 -07:00
Sora-bluesky
19a59f7d7b fix(compression): mirror the full trigger recomputation in the suggestion guard
Review follow-up on #67431 (hermes-sweeper):

- The viability check compared the floored percentage against the raw
  context window, but the built-in trigger recomputation also applies the
  output-token reservation, the 64K floor, and the degenerate-window
  guard (_compute_threshold_tokens). Mirror that math exactly, so e.g. a
  200K window with max_tokens=120K recomputes to max(0.75*80K, 64K)=64K
  and the suggestion is correctly KEPT for an 80K aux model instead of
  being suppressed by the raw-window percentage.
- Gate the built-in policy behind isinstance(ContextCompressor): external
  context engines own compaction policy (#44439), so plugin engines keep
  the plain suggestion untouched.
- The non-viable explanation now names the recomputed trigger instead of
  hardcoding the 75%/512K wording, so it stays accurate when the
  reservation (not the percentage floor) is what makes the value
  unreachable.

Tests: reservation-viability regression and plugin-engine passthrough,
per the review; the floored-branch assertion updated to the recomputed
number.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 06:58:58 -07:00
Sora-bluesky
dbc71fb6e4 fix(compression): don't recommend a threshold the small-context floor will ignore
The auxiliary-compression feasibility warning computes its
compression.threshold suggestion as aux_context / main_context,
independently of ContextCompressor._effective_threshold_percent()'s
raise-only small-context floor. For main windows under 512K the floor
raises any configured value below 75% back up, so a suggestion like
'threshold: 0.40' is silently ignored and the same warning returns every
session.

Derive the suggestion's viability through the compressor's own floor
logic: offer the 'lower the threshold' option only when the floored value
still fits the auxiliary model's context; otherwise recommend only a
larger compression model and explain the floor, so the guidance is always
actionable.

Tests: the updated auto-correct test pins the floored branch (no
threshold suggestion, floor explained); two new tests pin the surviving
suggestion at/above the floor on a small window and below 75% on a
512K+ window where no floor applies. The updated test fails against the
previous code.

Fixes #67422

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 06:58:58 -07:00
3ASiC
d46f0fb2d5 fix(compression): notify context engine after commit 2026-07-22 06:58:16 -07:00
WeiYusc
928bcdde24 fix(compression): refresh gateway activity during compaction
Refresh the agent activity tracker while context compression is blocked in the auxiliary summarizer so gateway watchdogs do not report inactivity during long compactions.

Add regression coverage for successful heartbeats, exception cleanup, touch failures, and strict-signature compressor fallback.

(cherry picked from commit c09e58b7709cc60c5b454701f0ecf840e759222f)
2026-07-22 06:57:33 -07:00
embwl0x
7de7a073c6 fix(compression): preserve synthetic user provenance 2026-07-22 06:56:53 -07:00
cucurigoo
377244f7c8 fix(compression): prevent stale-budget retry loops 2026-07-22 11:19:37 +05:30
PRATHAMESH75
0ba889d492 fix(agent): pass persisted-prefix boundary when rotation flushes on cold resume (#68196)
The legacy rotation branch in agent/conversation_compression.py flushes the
current turn to the OLD session before ending it (#47202) via
_flush_messages_to_session_db(messages) with no conversation_history boundary.

On the first turn after a cold Desktop resume, the restored transcript rows
live in the message list as plain dicts that have not yet been stamped with
_DB_PERSISTED_MARKER — the normal turn flush that stamps them runs after
preflight compression. With no boundary, _flush_messages_to_session_db builds
an empty history_ids set and treats every restored row as new, durably
re-appending the whole transcript to the parent session. Repeated
restart/resume + threshold compression keeps growing the parent transcript.

Pass messages[:_persist_user_message_idx] (the already-durable prefix that
turn_context anchors before preflight runs, guarded for int/bounds) as
conversation_history so the flush skips the persisted rows by identity and
writes only the current turn's new messages.

Adds a regression test that pre-populates SQLite, cold-loads the transcript,
appends one current user row, and forces rotating compression: it fails before
this change (parent grows to 5 rows) and passes after (parent holds the two
originals plus the single new turn).
2026-07-21 12:11:31 +05:30
Jan-Stefan Janetzky
766c617e83 fix(compression): detect semantic no-op results 2026-07-20 02:25:57 -07:00
Jan-Stefan Janetzky
46e4891c64 fix(compression): close post-dispatch lock scope 2026-07-20 02:25:57 -07:00
Jan-Stefan Janetzky
a48315e322 fix(compression): guard lock refresher startup 2026-07-20 02:25:57 -07:00
Jan-Stefan Janetzky
34bab1c6ac fix(compression): harden provider context handoff 2026-07-20 02:25:57 -07:00
Jan-Stefan Janetzky
192ef93ad5 fix(agent): harden pre-compress context handoff 2026-07-20 02:25:57 -07:00
Tranquil-Flow
ad8c533cc7 fix(agent): capture on_pre_compress return value and pass to compressor
The MemoryProvider.on_pre_compress() hook returns text that providers
want preserved in the compression summary, but run_agent.py discarded
the return value. Additionally, compress() and _generate_summary() had
no mechanism to accept this context.

- Capture on_pre_compress() return value in run_agent.py
- Add memory_context parameter to compress() and _generate_summary()
- Inject memory provider insights into summarization prompts

Fixes all MemoryProvider plugins that return context from
on_pre_compress() (currently silently broken for every plugin).
2026-07-20 02:25:57 -07:00
kshitijk4poor
86e603e7d6 fix(compression): verify cached prompt embeds current memory before retaining
The salvaged retention check compared the built-in memory snapshot
before vs after the disk reload. That holds for a long-lived CLI agent,
but on fresh-agent surfaces (gateway per-turn agents, TUI) the cached
prompt is restored from the session DB and can predate mid-session
memory writes that the fresh MemoryStore already absorbed at init: the
snapshot is then identical on both sides of the reload while the prompt
itself is stale, so compression would retain (and re-persist via
update_system_prompt) a prompt missing the new memory for the life of
the session.

Replace the equality check with a containment check
(_cached_prompt_reflects_builtin_memory): retain the cached prompt only
when the freshly-reloaded rendered blocks appear verbatim inside it,
and rebuild when a leftover block header remains for a target whose
entries have since been emptied or disabled. Block headers are shared
via MEMORY_BLOCK_HEADERS in tools/memory_tool.py so the check stays in
lockstep with MemoryStore._render_block.

Adds regression guards for the gateway stale-restore path and the
emptied-memory leftover-block path; verified with a real-MemoryStore
E2E matrix (9 scenarios) against a temp HERMES_HOME.
2026-07-20 13:13:02 +05:30
konsisumer
54c3f589ad fix(compression): retain prompt cache when memory is unchanged 2026-07-20 13:13:02 +05:30
ljy-2000
727392b5cb fix(context): revalidate compression state under lock 2026-07-19 13:16:52 +05:30
kshitijk4poor
c03c247e7c fix(compression): keep anchor restoration alternation-safe and grounding scaffolding-proof
Follow-up hardening on top of the salvaged #66637 commits:

- _insert_real_user_anchor could place the restored human turn directly
  next to user-role scaffolding (index-0 insert before a leading synthetic
  user turn, or a scaffolding-only transcript), breaking the strict
  alternation contract (#55677). Restoration now merges into trailing
  scaffolding (anchor text leads, synthetic flags cleared) and appends
  after a user-role compaction summary instead of inserting adjacent.
- _is_real_user_message now also rejects user-role compaction summaries
  (the compressor pins the summary to role=user when the tail opens with
  an assistant turn), so a summary can no longer satisfy the human-anchor
  check and skip restoration.
- _latest_user_task_snapshot reuses the same real-user predicate, so the
  deterministic task snapshot can no longer anchor on todo snapshots,
  truncation notices, or background-process reports.
- The Historical Task Snapshot rewrite keeps the section terminated with
  a blank line; the previous replacement consumed the boundary newlines,
  gluing the next '## ' heading mid-line and deleting all later sections
  on the next iterative compaction.
- Drop _length_continuation_synthetic (no producer anywhere).
- AUTHOR_MAP entry for enzo-adami.
2026-07-19 08:55:08 +05:30
Enzo Adami
960abf73a0 fix(compression): preserve human intent and durable handoffs 2026-07-19 08:55:08 +05:30
kshitijk4poor
af7dceaf77 fix(context): persist fallback compaction breaker 2026-07-14 02:19:40 +05:30
kshitijk4poor
2627933f33 fix(agent): distinguish missing from broken compression locks 2026-07-14 00:14:55 +05:30
Rory Ford
8f29c9f4e3 fix(agent): fail closed on unexpected compression-lock acquisition errors
Splits the single broad `except Exception` in the compression-lock
acquire path into two handlers: AttributeError/TypeError (version skew —
the lock method is missing, or predates the `ttl_seconds=` kwarg) still
fails OPEN as before, since that's known-safe to proceed without a lock.
Any other exception now fails CLOSED (skips compression this cycle)
instead of treating every failure as "no lock subsystem present" and
letting a second compressor run concurrently and fork the session.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 00:14:55 +05:30
kshitijk4poor
8121dbb166 fix(codex): reject interrupted manual compaction 2026-07-11 22:59:49 +05:30
kshitijk4poor
8c62a92296 fix(codex): consume manual compaction usage gaps 2026-07-11 22:59:49 +05:30
kshitijk4poor
83000c7295 fix(compaction): clear stale anti-thrash verdicts 2026-07-11 22:59:49 +05:30
kshitijk4poor
2c6e5877a6 fix(compaction): arm verdict after successful boundary 2026-07-11 22:59:49 +05:30
hmirin
d1c8c03416 feat(agent): add Codex-native compaction paths 2026-07-07 02:39:54 -07:00
kshitijk4poor
d8504df7e4 refactor(compression): reuse _fresh_compaction_message_copy in user-turn guard
Replace the inline dict-copy + _db_persisted pop in
_ensure_compressed_has_user_turn with the canonical
_fresh_compaction_message_copy helper (the same primitive the compressor's
own protected-head/tail assembly uses), so the persistence-marker strip
stays consistent across all compaction copy sites (#57491). Expand the
docstring to record the alternation-safety and end-placement rationale.
2026-07-04 21:04:27 +05:30
yoma
6e176e4c21 fix(compression): preserve user turn after compaction 2026-07-04 21:04:27 +05:30
kshitijk4poor
58d8e25e67 fix(agent): make compression lock-lease refresher tolerate transient DB blips
Follow-up hardening on the salvaged #54465 backoff persistence work.

The lease refresher's loop treated ANY falsy refresh as a permanent stop
(`if not refreshed: break`), conflating two distinct cases:
  - genuine lost-ownership (rowcount 0) — correct to stop, and
  - a one-off transient DB error (write contention that escapes
    _execute_write's retry budget) — which returned False identically.

A single transient blip therefore killed the lease for the rest of a
multi-minute compression call, silently reintroducing the exact 300s-TTL <
~361s-call expiry wedge the PR set out to fix.

Changes:
- _CompressionLockLeaseRefresher._run now tolerates a bounded run of
  consecutive failures (_MAX_CONSECUTIVE_REFRESH_FAILURES = 3) before giving
  up the lease; a recovered tick resets the counter. Worst-case extra hold is
  cap * refresh_interval, still bounded by the acquirer's TTL.
- Replace the two remaining silent `except Exception: pass` arms in the
  compression-failure-cooldown persist/clear helpers with debug logging, for
  parity with their sqlite3.Error sibling arms (a non-sqlite bug was invisible).
- Document the join(timeout=1.0) quiesce bound in stop().
- Add 3 regression tests: single-blip tolerance, persistent-failure stop at the
  cap, and refresh-raising tolerance.
2026-06-30 13:36:29 +05:30
Rod Boev
cafe9d9261 fix(agent): prevent stale lock leases after early compression exits (#54465) 2026-06-30 13:36:29 +05:30
Rod Boev
f2ace45286 fix(agent): release refreshed compression locks on every exit path (#54465) 2026-06-30 13:36:29 +05:30
Rod Boev
53ef954841 fix(agent): keep cooldown and lock refresh on one authority (#54465) 2026-06-30 13:36:29 +05:30
Rod Boev
f2ccb2859f fix(agent): persist compression backoff across resume (#54465) 2026-06-30 13:36:29 +05:30
diamondeyesfox
8df231c941 fix(agent): rebaseline in-place compression flushes 2026-06-27 03:04:26 -07:00
herbalizer404
b82c83d320 fix(auxiliary): honor fallback chain when compression provider auth is unavailable
When an explicit aux provider cannot build a client before any request is
sent (missing raw env key, exhausted/unavailable OAuth or credential-pool
auth, resolver returning (None, None)), call_llm raised a misleading
"no API key was found" error and bypassed the configured fallback_chain
entirely. A provider authenticated through Hermes auth / the credential
pool (e.g. ollama-cloud) whose pool entry is exhausted hit this path, so
compression failed instead of routing to the configured fallback.

Adds _try_configured_fallback_for_unavailable_client() and wires it into
both sync and async call_llm before the raise, and into the startup
compression feasibility check.

Salvaged from #51835 by @herbalizer404.
2026-06-25 13:08:18 -07:00
kshitijk4poor
e0272cfef2 Revert "fix(compression): make minimum context floor configurable (#31600)"
This reverts commit cae1ee44a7.
2026-06-25 01:04:44 +05:30
Tranquil-Flow
cae1ee44a7 fix(compression): make minimum context floor configurable (#31600)
Add compression.minimum_context_floor config key that allows users
to lower the compression threshold floor below the hardcoded 64K
default, preventing infinite tool-call loops on models whose
structured output degrades well before 64K tokens.

- agent/model_metadata.py: add get_configurable_minimum_context()
  helper with 16K hard safety limit
- agent/context_compressor.py: accept minimum_context_floor param,
  thread it through _compute_threshold_tokens
- agent/conversation_compression.py: use compressor's floor for
  aux model context validation
- agent/agent_init.py: read compression.minimum_context_floor from
  config and pass to ContextCompressor
- gateway/run.py: cache-busting includes new key

Salvaged from #31686 by @Tranquil-Flow onto current main.
Resolves conflicts with in-place compaction (#38763) and max_tokens
threshold computation (#43547) that landed after the original PR.

Closes #31600
2026-06-25 00:56:04 +05:30
Brooklyn Nicholson
88e136448d fix(agent): shrink anthropic-native image history
Retry image-size rejections by rewriting Anthropic base64 image source blocks, not just OpenAI-style image_url parts.
2026-06-22 18:23:21 -05:00