Commit Graph

224 Commits

Author SHA1 Message Date
Teknium 017906fb4f refactor(agent/conversation_compression): flatten feasibility probe call shape 2026-09-02 23:42:15 -07:00
Teknium bcdd78e3c3 refactor(agent/conversation_compression): compact fence/constant docstrings and comments; hug remaining docstring bodies 2026-09-02 23:38:31 -07:00
Teknium 566adb178d refactor(agent/conversation_compression): compact docstrings — summary line hugs its prose body (text unchanged) 2026-09-02 23:30:28 -07:00
Teknium 58d9f24663 refactor(agent/conversation_compression): AST-identical — drop paragraph blanks inside function bodies 2026-09-02 23:26:28 -07:00
Teknium 01ebcde193 refactor(agent/conversation_compression): fold in-place/rotation post-commit writes into their branches; heartbeat stop helper 2026-09-02 23:23:38 -07:00
Teknium 4b041206d0 refactor(agent/conversation_compression): try/except-assign -> suppress with defaults, fold shrink accept gate, flatten warning text assembly 2026-09-02 23:21:35 -07:00
Teknium 3675c9d83a refactor(agent/conversation_compression): collapse guard ladders, dict/payload builders and duplicated temporaries 2026-09-02 23:14:55 -07:00
Teknium 02ef6480de refactor(agent/conversation_compression): AST-identical — pack trailing log args after multi-line format strings 2026-09-02 23:08:53 -07:00
Teknium 8998c13bcd refactor(agent/conversation_compression): reflow prose docstrings to 110 cols (text unchanged) 2026-09-02 23:06:46 -07:00
Teknium 0fa8263bf8 refactor(agent/conversation_compression): drop pre-return lease releases already covered by the finally 2026-09-02 23:03:21 -07:00
Teknium e0b3879d6b refactor(agent/conversation_compression): AST-identical layout — hug bracket spans, merge split string literals 2026-09-02 22:51:28 -07:00
Teknium a6dd9dd07a refactor(agent/conversation_compression): collapse redundant guards and defensive layers 2026-09-02 22:46:53 -07:00
Teknium 53800aab9d refactor(agent/conversation_compression): drop duplicated comments 2026-09-02 20:14:49 -07:00
Teknium 6082f7d442 refactor(agent/compression): join remaining fragmented log/warning literals (AST-identical) 2026-09-02 20:13:50 -07:00
Teknium 551fe6ea78 refactor(agent/conversation_compression): fold heartbeat suppress predicate, lease-refresh swallow, cooldown capture ordering 2026-09-02 19:57:49 -07:00
Teknium 3a1e4a7661 refactor(agent/compression): fold boundary bookkeeping locals; pack one-item-per-line call/def argument lists (AST-identical) 2026-09-02 19:51:06 -07:00
Teknium 264cc6bd99 refactor(agent/conversation_compression): thread an _Attempt carrier through compress_context phases 2026-09-02 19:45:08 -07:00
Teknium fa4fc3ea1b refactor(agent/compression): reflow docstrings/comment blocks to 118 cols (word-identical) 2026-09-02 19:39:34 -07:00
Teknium ec0984c62b refactor(agent/conversation_compression): flatten _strip_stale_todo_snapshot list branch 2026-09-02 19:37:58 -07:00
Teknium 0419866f01 refactor(agent/conversation_compression): fold anchor placement, steer markers, orphan reopen, timeout config parsing 2026-09-02 19:37:08 -07:00
Teknium e0d7f18fc4 refactor(agent/compression): drop blank lines after function-local imports (AST-identical) 2026-09-02 19:31:44 -07:00
Teknium 64215c980c refactor(agent/conversation_compression): log-and-swallow try/except -> _swallow() context manager (27 sites) 2026-09-02 19:30:28 -07:00
Teknium 4fc0d74fe3 refactor(agent/compression): try/except-pass -> contextlib.suppress (21 sites) 2026-09-02 19:24:14 -07:00
Teknium 7bd7c1114a refactor(agent/compression): join fragmented adjacent string literals (AST-identical) 2026-09-02 19:19:41 -07:00
Teknium 4b0116025b refactor(agent/conversation_compression): lift durable lock acquire + contention sit-out out of _acquire_compression_lease 2026-09-02 19:13:27 -07:00
Teknium ec16f113a4 refactor(agent/compression): reflow to 120 columns (AST-identical); joins fragmented call/string continuations 2026-09-02 19:08:36 -07:00
Teknium 37b0aaa530 refactor(agent/conversation_compression): collapse duplicated commit rollback branches 2026-09-02 19:07:05 -07:00
Teknium ac7591019e refactor(agent/conversation_compression): extract codex route and start-announcement from compress_context 2026-09-02 19:01:49 -07:00
Teknium 9672db5399 refactor(agent/conversation_compression): extract attempt prologue and lease sit-out helper; fold codex failure flag 2026-09-02 18:59:05 -07:00
Teknium 0f60feec52 refactor(agent/conversation_compression): lift feasibility threshold-lowering warning out of check_compression_model_feasibility 2026-09-02 18:54:31 -07:00
Teknium 0e9d46511a refactor(agent/conversation_compression): lift image-shrink nested helpers, lease lock-API resolution, rotation state carry-over, durable cooldown rollback 2026-09-02 18:49:03 -07:00
Teknium 58c7789e36 refactor(agent/conversation_compression): extract summary phase of compress_context and the timeout wrapper's wait/cancel/unwind helpers 2026-09-02 18:33:49 -07:00
Teknium 719cbb6e55 refactor(agent/conversation_compression): extract SessionDB commit phase; fold automatic gate + codex fence bracket 2026-09-02 18:27:34 -07:00
Teknium 7524e9ec8a refactor(agent/compression): unify session rebind, scoped-twin stamping; lift facade timeout callbacks to module helpers 2026-09-02 18:17:52 -07:00
Teknium 3a2eceabc8 refactor(agent/compression): remove dead memory-containment helpers; fold codex skip-log ladder
- delete _cached_prompt_reflects_builtin_memory + _builtin_memory_prompt_snapshot
  (zero callers since the commit site moved to byte-equality; the only test
  reference asserts their ABSENCE from the commit window)
- delete CompressionExecutorSaturatedError (never raised or caught anywhere)
- _compress_context_via_codex_app_server: three near-identical skip branches
  collapse into one skip_reason + single log line (same message text)
2026-09-02 13:29:37 -07:00
Teknium 7e3cef7d87 refactor(agent/compression): extract candidate rejection ladder from compress_context
_candidate_rejected(): compressor abort, no-progress, empty transcript and
superseded-attempt checks in their original order; caller handles the
shared release + unchanged-return. Aborted-branch try/finally collapsed
(the finally only released the lease, which the caller now does).
2026-09-02 13:29:37 -07:00
Teknium 0ea4c7963a refactor(agent/compression): extract post-commit boundary tail of compress_context
- _finish_compaction_boundary(): parent label clear, context-engine + memory
  provider notifications, compression-count warning, session:compress hook,
  usage re-arm, file/skill dedup reset (zero back-refs; returns rough estimate)
- _warn_summary_or_aux_fallback(): once-only summary/aux-model warnings
compress_context 794 -> 653 lines; bodies moved verbatim.
2026-09-02 13:29:37 -07:00
Teknium 01d16451f7 refactor(agent/compression): extract commit-phase helpers out of compress_context
- _fold_todo_snapshot(): stale-snapshot strip + live todo fold into the tail
- _rebuild_system_prompt_at_boundary(): tool refresh + byte-equal keep-prompt
- _salvage_or_refuse_grown_transcript(): commit-site anti-growth guard
- _publish_rotated_compaction(): parent flush, child publish, id re-point,
  goal/heartbeat/loop/title carry-over
- old_session_id is now an explicit Optional local instead of locals().get()
compress_context 1181 -> 794 lines; bodies moved verbatim.
2026-09-02 13:29:37 -07:00
Teknium cbd709124f refactor(agent/compression): extract pre-summary phases of compress_context
- _adopt_grown_durable_parent(): rotation-only durable-snapshot adoption
- _pre_compress_memory_context(): provider on_pre_compress / checkpoint gate
- _resolve_compress_call(): compress() + signature-filtered kwargs
- _run_summary_dispatch(): progress hook, stream deadline, interrupt guard
compress_context 1359 -> 1181 lines; bodies moved verbatim.
2026-09-02 13:29:37 -07:00
Teknium 42c41dfcff refactor(agent/compression): extract lease acquisition and lifecycle out of compress_context
- _CompactionLifecycle: the one-shot terminal status edge (was a nonlocal closure)
- _CompressionLease: holder/watermark/ttl + refresher, holder-only release,
  fence lock-setup bracket, release() (was 4 nested closures + 9 locals)
- _acquire_compression_lease(): the 190-line lock acquisition ladder
- _adopt_if_parent_rotated(): late-contender adoption check
compress_context 1767 -> 1359 lines; bodies moved verbatim.
2026-09-02 13:29:37 -07:00
Teknium 852f2a02d8 refactor(agent/compression): unify repeated abort-path snippets in compress_context
- _existing_system_prompt: 21 copies of cached-or-rebuild prompt
- _emit_aborted_attempt_telemetry: 11 copies of aborted/aborted telemetry
- _restore_messages_snapshot: 4 copies of deepcopy rollback
- _restore_prune_rearm_tokens: 3 copies of prune-runway restore
2026-09-02 13:29:37 -07:00
Teknium 682d48c48a refactor(agent/compression): compact comments and docstrings in conversation_compression (AST-identical) 2026-09-02 13:29:37 -07:00
Teknium d1efa0d78d fix(compression): provider-proven overflow gets one real compaction attempt while the failure cooldown is armed
After one failed/stalled summary attempt arms the 60/300/900s compression-
failure cooldown, a provider context_length_exceeded rejection entered the
reactive overflow branch in conversation_loop, which called _compress_context
without force. Since #97488 the cooldown gate returns the soft "temporarily
paused, retry in a moment" deferral instead of exhaustion, so every turn
deferred until the cooldown lapsed, and the next failure extended the ladder:
long-running sessions wedged with no automatic recovery (#100661, four sessions
lost).

Thread a narrow `bypass_cooldown` kwarg from the three provider-proven overflow
call sites (generic overflow, 413, output-cap recovery) through
AIAgent._compress_context -> compress_context -> ContextCompressor.compress ->
_generate_summary. It skips ONLY the summary-failure cooldown check at each gate.
Unlike force=True it does not clear the cooldown, does not skip the feasibility /
anti-thrash breakers, and a failed attempt records its cooldown normally. The
attempt is bounded by the existing compression_attempts/max_compression_attempts
budget, so there is no retry loop. The preflight threshold gate is unchanged:
ordinary over-threshold pressure still honors the cooldown (#11529).

Engines whose _automatic_compression_blocked()/compress() predate the kwarg
(plugins, test doubles) are called with the legacy signature.

Tests: cooldown armed + bypass_cooldown -> summarizer invoked and transcript
compacted; ordinary pass still deferred. Docs note the cooldown/overflow
contract in the developer guide.

Fixes #100661
Closes #97766 (overflow-force idea; the bundled continuation changes were not taken)

Co-authored-by: sgtworkman <178342791+sgtworkman@users.noreply.github.com>
2026-09-02 05:33:22 -07:00
Teknium bc71b8bc95 fix(compression): anchor on the LAST intent row — newer user turn outranks older steer (#100053 follow-up)
Follow-up to the salvaged #100114 commit. Its two-pass anchor selection
scanned steers first and real user rows second, so a transcript shaped
[user A, tool(steer B), ..., user C] anchored the already-consumed steer B
over the newer real request C — the same replay class the PR set out to
fix. Replace it with one reversed positional scan that picks whichever
intent-bearing row is last (real role=user or steer-bearing role=tool),
and make the compressed-transcript steer check count only role=tool rows
(the only place the runtime delivers a steer), so a summary quoting the
marker cannot masquerade as live intent.

Adds S1/S2/S3 regression tests (steer dropped by compaction, steer
surviving in tail, newer user turn after steer) plus alternation and
use-exactly-once assertions.
2026-09-02 04:12:12 -07:00
finn763 f40333a80e fix(agent): preserve busy steer during compression and avoid replaying historical user request
Compression with display.busy_input_mode: steer embeds the follow-up
as an out-of-band marker inside the latest role=tool result. The
post-compression user-turn preservation path only classified
non-scaffolding role=user rows as real intent, so a compressed
transcript that contained no role=user row would discard the steer
and clone an older historical role=user message as the new active
turn, re-activating a previously consumed request.

Fix _ensure_compressed_has_user_turn to (1) treat a compressed
transcript that already carries a steer marker as having user intent,
and (2) prioritize the latest steer payload from the original
transcript over historical user cloning, inserting it as a proper
role=user turn via _insert_real_user_anchor. This preserves the
actual current intent exactly once and never turns history into new
input.

Closes #100053
2026-09-02 04:12:12 -07:00
Teknium 9de9d7613c fix(compression): keep hygiene turn-hold worker's commit admission so thinking-model summaries are adopted, not burned
The 10s hygiene_max_turn_hold_seconds budget (#92318) releases the arriving
user turn while the summary model is still streaming. For thinking summary
models (DeepSeek-V4-Flash etc.) whose reasoning prefix alone exceeds 10s,
the abandonment path ALWAYS cancelled the commit fence — 100% of the summary
attempt (including the full thinking prefix) was discarded on every turn,
permanently disabling auto-compression while paying the summary model 10s
of thinking per turn, and the flat 60s retry-after then blocked the
agent-side preflight from a fresh chance.

Structural fix (maintainer-chosen direction in #97963): decouple the turn
from the compression instead of holding the turn longer or making the hold
progress-aware (which would reintroduce the #90845 frozen-turn bug):

- CompressionCommitFence gains mark_commit_watermark_fenced() /
  commit_watermark_fenced; compress_context marks the fence right after
  capturing get_active_message_watermark() under the durable compression
  lock (#75316/#87484) — the property that makes a LATE commit safe: rows
  appended after compression start survive both commit paths verbatim as
  cloned concurrent tail (archive_and_compact watermark= and
  publish_compression_child watermark/watermark_ceiling).
- gateway hygiene turn-hold handler: when the fence is watermark-fenced,
  the detached worker (already kept alive via
  _defer_agent_cleanup_until_future_done) KEEPS its commit admission; the
  user's turn proceeds on the uncompressed transcript at the same 10s
  budget, and the summary is adopted at the worker's own watermark-fenced
  commit boundary. Unfenced workers are cancelled exactly as before —
  never worse than the status quo.
- No retry-after is armed while the kept-admission attempt runs (it would
  block preflight adoption via the same-session cooldown); re-attempt
  spacing is covered by the durable compression lock
  (_session_has_compression_in_flight). If the worker ends WITHOUT
  committing, a done-callback restores the flat non-escalating 60s
  retry-after; a successful adoption resets the hygiene failure streak.
  The streak never advances for a deferral either way.
- Docs: configuration.md hygiene_max_turn_hold_seconds one-liner updated
  to describe deferred adoption and the thinking-model case;
  config_defaults.py comment updated. Knob stays config.yaml-only.

Invariants preserved:
- 10s user-latency cap stays hard (#90845/#92318):
  test_session_hygiene_turn_hold_budget_abandons_streaming_wait passes
  UNMODIFIED (its worker is not watermark-fenced, so it pins the cancel
  path through the public surface).
- Stale-clobber impossible: adoption only rides commits bounded by the
  start watermark; the fence still gates admission and unfenced/late
  results are discarded.

New regression tests (tests/gateway/test_session_hygiene_turnhold_adoption.py):
- watermark-fenced worker keeps admission, late summary is committed,
  turn still released at the budget, no cooldown while running,
  streak reset on adoption;
- kept-admission worker that ends without committing restores the flat
  turn-hold retry-after (<=120s, names turn-hold, streak untouched);
- unfenced worker still cancelled and discarded (status quo).
Sabotage-verified: disabling the keep-admission branch fails the two new
adoption tests and leaves the unfenced-cancel test green.

Fixes #97963
2026-09-01 23:56:23 -07:00
joaomarcos 904e5bb572 fix(compression): stop the summary stream at the host's own deadline
CompressionCommitFence.set_total_ceiling_seconds documents its deadline as
"shared by the host and worker", but only the host ever read it. The worker's
streamed summary bounds itself with _aux_stream_total_ceiling() instead —
max(600, 4 * aux_timeout) — which is >= the host's total ceiling for every
configured timeout AND starts counting later (after pool admission,
_serialize_for_summary, prompt build and TTFT). A stream that outlives its
abandoned host is therefore not an edge case; it is the guaranteed outcome of
every total-ceiling timeout.

8207862212 closed the first half: a cancelled fence now releases the
compression owner, freeing its pool slot and session lease. Its own comment
leaves the second half open — the isolated provider daemon that holds the
socket keeps streaming "until the auxiliary stream's longer absolute ceiling
expires". With the #99692 reporter's auxiliary.compression.timeout: 600 that
is 2400s of an orphaned ~500K-token summary the fence is already guaranteed to
refuse, and because the session never shrank, every following turn stacks a
fresh orphan on top of the last.

Publish the fence's deadline as an absolute monotonic instant
(CompressionCommitFence.deadline_monotonic) and give the auxiliary layer the
return leg it was missing: aux_stream_deadline() installs it thread-locally,
_ChatStreamAccumulator.feed() stops the stream once it passes, and
_run_protected_sync_provider_call propagates it onto the provider daemon
(thread-locals do not cross that boundary, so an owner-thread-only install
would be inert on exactly the path large-session compression takes).

Absolute, not relative: the deadline is unaffected by however long dispatch and
TTFT took before the accumulator was constructed. Checked as well as — not
instead of — the existing ceiling, so every caller without a host deadline is
byte-for-byte unchanged, and the "timed out" phrasing keeps _is_timeout_error
classification identical to a request timeout.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EKrRS7LVgyHf2WQkEahSwu
2026-09-01 23:56:06 -07:00
Teknium c0495c6bce fix(cli): context meter no longer sawtooths on reasoning models — show durable transcript, not last-request replay
On reasoning models a long tool loop replays the current turn's thinking +
scaffolding on every request, so the LAST request's prompt_tokens can exceed
the durable transcript by hundreds of K — all of which evaporates at the turn
boundary. The status bar and /context breakdown rendered that raw figure, so
users watched 'context' jump (e.g.) 850K -> 600K across a turn boundary and
read it as a broken compaction.

- conversation_loop: capture a turn-base usage anchor from the turn's FIRST
  provider response (api_call_count == 1), where replay is minimal.
- anchored_context_tokens: new charge_stale_thinking kwarg forwarded to the
  delta estimate (stale reasoning excluded on all but the newest assistant
  message).
- cli status snapshot + context_breakdown: prefer the turn-base anchored
  figure; fall back to last-response anchor / raw last_prompt_tokens.
- All _usage_anchor invalidation sites also clear _turn_base_usage_anchor.

Display-only: compression trigger math keeps using real last-request usage
(the inflated request is what actually risks the window mid-loop).
2026-09-01 15:34:03 -07:00
joaomarcos ecdbcef7af fix(compression): roll the live transcript back when an in-place compaction commit fails (#99477)
`archive_and_compact()` is atomic: when it raises, every pre-compaction row is
still `active = 1` and the compacted set was never inserted. The rotation branch
already rolled the live transcript back to `messages_before_compression` in that
case, but the in-place branch — the default (`compression_in_place` defaults to
True) — did not, so `compress_context()` handed the caller the uncommitted
compacted list.

That list is marker-swept by `_strip_persistence_markers` (#57491) and the
post-commit `stamp_db_persisted_markers` (#98450) never ran, so the next
append-only flush treated the whole compacted transcript as new and INSERTed it
on top of the rows it was supposed to replace. The active set then held the
summary AND the turns it summarized: the next resume reloaded both, the token
count went up, preflight fired again, and every failed attempt appended another
copy of the protected head plus tail.

The in-place rollback mirrors the rotation branch and is gated on
`split_status != "in_place_committed"`, which is assigned on the statement
immediately after the atomic commit returns, so a committed compaction can never
be rolled back into a mismatch of the opposite sign.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EKrRS7LVgyHf2WQkEahSwu
2026-09-01 09:28:31 -07:00
theo 8207862212 fix(compression): stop timeout paths from blocking retries 2026-08-31 13:00:33 -07:00