fix(context): emergency compaction — necessity vs utility split with hysteresis

The submarine thrash class: once transcript+frame exceeded the trigger,
_run_round_compaction fired EVERY round — a light-model call plus a transcript
rewrite that collapsed the provider prompt cache to the static floor — because
the trigger counted the un-compactable frozen frame (wave3/low: 35/35 rounds,
the 400K-char LOW trigger sits below the irreducible low-mode frame; wave1:
24 passes with cached=338,944 streaks).

- NECESSITY (compact at all?) stays TOTAL pressure — the frame counts toward
  the provider window (an immutable core overflowing the window must still
  trigger) — but is now compared in CALIBRATED real tokens: the char budget's
  documented token equivalent vs chars/4 x main_loop_token_density
  (context_fit SSOT: neutral 1.0 cold, measured supersedes — deliberately NOT
  resolve_token_density's review-pack cold-conservative value, which would
  demote fresh installs; the v6.80->v6.81 oscillation). On a measured ~1.7x
  Claude route the trigger now fires at its intended token budget instead of
  ~1.7x past it; cold stores behave byte-for-byte as before.
- UTILITY/rearm (can a pass help; when refire?) is the COMPACTABLE region only
  (transcript beyond the frozen frame): a pass that could NOT get below the
  trigger arms a hysteresis in accumulated_usage — no refire until the region
  grows >=1.2x or 10 rounds pass (context_budget constants). One loud
  emit_progress disclosure + a typed compaction_hysteresis_armed checkpoint
  event on arming replace the per-round call/rewrite.
- The reactive provider-overflow one-shot low-retry net is untouched, as are
  manual (pending) and routine compaction branches.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Ouroboros 2026-08-08 05:56:13 +03:00
parent d6928b95cf
commit 02fecbc019
4 changed files with 223 additions and 15 deletions

View file

@ -23,8 +23,24 @@ from __future__ import annotations
# Main-loop emergency tool-history compaction trigger (~300K tokens at chars/4).
# Remote routine compaction stays off by design; this is the overflow backstop.
# NECESSITY is judged on this budget in CALIBRATED real tokens (chars/4 × the
# main-loop measured density, neutral 1.0 cold): on a ~1.7×-dense Claude route
# the raw-char form silently meant ~500K real tokens, engaging only deep into
# the task and then thrashing (see the hysteresis constants below).
EMERGENCY_COMPACTION_CHARS = 1_200_000
# Emergency-compaction hysteresis (the necessity-vs-utility split). NECESSITY
# (compact at all?) is total calibrated pressure — the frozen frame (system
# blocks + tools + protected/kept rounds) counts toward the provider window.
# UTILITY (can a pass help?) is the COMPACTABLE region only: after a pass that
# could NOT get the context below the trigger (the frame alone exceeds it —
# the submarine wave3 shape: 35/35 rounds fired, each pass a light-model call
# plus a transcript rewrite that collapsed the prompt cache to the static
# floor), further passes are suppressed until the compactable transcript grows
# by this factor or this many rounds pass, whichever is first.
COMPACTION_HYSTERESIS_REGION_GROWTH = 1.2
COMPACTION_HYSTERESIS_ROUNDS = 10
# Background-consciousness assembled-context guards. P1: fail fast, never
# silently truncate cognitive artifacts.
BG_CONTEXT_WARN_CHARS = 600_000 # ~150K tokens: warn but proceed

View file

@ -197,6 +197,30 @@ def estimate_context_prompt_tokens(
return max(0, int(total))
def main_loop_token_density(drive_root: Any, model: str) -> float:
"""MAIN-LOOP calibrated token density: neutral 1.0 cold, measured supersedes.
The baseline `_route_calibration_ratio` starts from, exposed as its own SSOT so
the emergency-compaction necessity trigger and the fit projections share ONE
policy. DELIBERATELY NOT ``capability_evidence.resolve_token_density`` that is
the review-pack COLD-CONSERVATIVE value, which on an empty observation store
(every fresh install and isolated benchmark server) would silently narrow the
main loop's horizon on a guess (the v6.80.0 → v6.81.0 oscillation; BIBLE P1).
Only a MEASURED density for this exact model identity may raise it above 1.0.
"""
baseline = 1.0
try:
from ouroboros.capability_evidence import get_token_density
from ouroboros.provider_models import normalize_model_identity
measured = get_token_density(drive_root, normalize_model_identity(str(model or "")))
if measured > 0:
baseline = measured
except Exception:
log.debug("Measured token density unavailable", exc_info=True)
return baseline
def _route_calibration_ratio(
drive_root: pathlib.Path,
route_fp: str,
@ -225,17 +249,7 @@ def _route_calibration_ratio(
fresh evidence store; the first successful send records this model's density, after
which the projection is measured rather than guessed.
"""
baseline = 1.0
try:
from ouroboros.capability_evidence import get_token_density
from ouroboros.provider_models import normalize_model_identity
measured = get_token_density(drive_root, normalize_model_identity(str(model or "")))
if measured > 0:
baseline = measured
except Exception:
log.debug("Measured token density unavailable for context fit", exc_info=True)
ratios = [float(baseline)]
ratios = [float(main_loop_token_density(drive_root, model))]
try:
events_path = pathlib.Path(drive_root) / "logs" / "events.jsonl"
for event in iter_jsonl_objects(

View file

@ -21,7 +21,12 @@ from ouroboros.observability import new_call_id, persist_call
from ouroboros.tool_policy import CAPABILITY_OMISSION_HEADER, format_capability_omissions, initial_tool_schemas, list_non_core_tools, swarm_router_turn
from ouroboros.tools.registry import ToolRegistry
from ouroboros.context import build_user_content, estimate_context_prompt_tokens
from ouroboros.context_budget import EMERGENCY_COMPACTION_CHARS, LOW_EMERGENCY_COMPACTION_CHARS
from ouroboros.context_budget import (
COMPACTION_HYSTERESIS_REGION_GROWTH,
COMPACTION_HYSTERESIS_ROUNDS,
EMERGENCY_COMPACTION_CHARS,
LOW_EMERGENCY_COMPACTION_CHARS,
)
from ouroboros.context_compaction import _tool_round_spans, compact_tool_history_llm
from ouroboros.deadline_utils import parse_deadline_ts, utc_now
from ouroboros.utils import estimate_tokens, truncate_review_artifact
@ -3144,27 +3149,87 @@ def _run_round_compaction(
# compaction; max => 1.2M-char emergency-only (cache-friendly). No per-model
# window table; the reactive provider-overflow detector (context.py) drops the
# agent to low mode if a route's real window turns out smaller than assumed.
#
# NECESSITY vs UTILITY (the submarine thrash fix). NECESSITY — should we
# compact at all? — is TOTAL calibrated pressure: the frozen frame (system
# blocks, tools, protected/kept rounds) counts toward the provider window even
# though no pass can shrink it, and the char budget is compared in CALIBRATED
# real tokens (main_loop_token_density: neutral 1.0 cold, measured supersedes
# — never the review-pack cold-conservative value, which would demote fresh
# installs; the v6.80→v6.81 oscillation). UTILITY — can a pass help, and when
# should it refire? — is the COMPACTABLE region only (the transcript beyond
# the frozen frame): a pass that could NOT get below the trigger arms a
# hysteresis, one loud disclosure replaces the per-round light-model call +
# cache-destroying rewrite (wave3: 35/35 rounds fired because the LOW trigger
# sits below the irreducible low-mode frame), and the trigger re-arms only
# when the region grows ~20% or after N rounds. The reactive provider-overflow
# low-retry net (one-shot, loop exit path) is deliberately untouched.
emergency_chars = LOW_EMERGENCY_COMPACTION_CHARS if ctx.active_context_mode == "low" else EMERGENCY_COMPACTION_CHARS
if _estimate_messages_chars(messages) > emergency_chars:
from ouroboros.context_fit import main_loop_token_density
density = main_loop_token_density(ctx.drive_root, ctx.active_model)
threshold_real_tokens = emergency_chars / 4.0 # the token budget the char constant documents
pressure_real_tokens = (_estimate_messages_chars(messages) / 4.0) * density
if pressure_real_tokens > threshold_real_tokens:
usage_state = getattr(ctx.tools._ctx, "_accumulated_usage", None)
usage_state = usage_state if isinstance(usage_state, dict) else {}
spans = _tool_round_spans(messages)
region_chars = _estimate_messages_chars(messages[spans[0][0]:]) if spans else 0
hysteresis = usage_state.get("_compaction_hysteresis")
if isinstance(hysteresis, dict):
armed_region = int(hysteresis.get("region_chars") or 0)
armed_round = int(hysteresis.get("round") or 0)
if (
region_chars < armed_region * COMPACTION_HYSTERESIS_REGION_GROWTH
and (ctx.round_idx - armed_round) < COMPACTION_HYSTERESIS_ROUNDS
):
return messages, None # armed: a pass cannot help yet (disclosed once, on arming)
usage_state.pop("_compaction_hysteresis", None)
# keep_recent must stay BELOW the current span count or the compactor
# no-ops (len(spans) <= keep_recent returns as-is): a transcript over
# the emergency byte threshold with only ~50 huge rounds previously
# never compacted at all. Halve the history (floor 6), but ALWAYS
# clamp below the span count so even 2-6 huge rounds compact; with a
# single round there is nothing older to summarize.
span_count = len(_tool_round_spans(messages))
span_count = len(spans)
emergency_keep_recent = min(50, max(6, span_count // 2), max(1, span_count - 1))
if _persist_compaction_checkpoint(
messages, drive_root=ctx.drive_root, drive_logs=ctx.drive_logs, task_id=ctx.task_id,
reason="emergency_context_size", keep_recent=emergency_keep_recent,
round_idx=ctx.round_idx, event_queue=ctx.event_queue,
):
return compact_tool_history_llm(
messages, usage = compact_tool_history_llm(
messages,
keep_recent=emergency_keep_recent,
drive_root=ctx.drive_root,
task_id=ctx.task_id,
)
after_real_tokens = (_estimate_messages_chars(messages) / 4.0) * density
if after_real_tokens > threshold_real_tokens:
spans_after = _tool_round_spans(messages)
region_after = _estimate_messages_chars(messages[spans_after[0][0]:]) if spans_after else 0
usage_state["_compaction_hysteresis"] = {
"round": ctx.round_idx,
"region_chars": region_after,
}
ctx.emit_progress(
"⚠️ Emergency compaction was futile: calibrated context "
f"{after_real_tokens / 1000:.0f}K real tokens still exceeds the "
f"{threshold_real_tokens / 1000:.0f}K trigger (the frozen frame cannot be "
"compacted). Further passes suppressed until the compactable transcript "
f"grows ≥{COMPACTION_HYSTERESIS_REGION_GROWTH:.1f}x or "
f"{COMPACTION_HYSTERESIS_ROUNDS} rounds pass."
)
_emit_checkpoint_event(ctx.event_queue, ctx.task_id, ctx.drive_logs, {
"checkpoint_kind": "compaction_hysteresis_armed",
"round": ctx.round_idx,
"reason": "emergency_pass_futile",
"calibrated_real_tokens": int(after_real_tokens),
"threshold_real_tokens": int(threshold_real_tokens),
"compactable_region_chars": region_after,
"token_density": round(float(density), 3),
})
return messages, usage
ctx.emit_progress("⚠️ Emergency compaction skipped: forensic checkpoint could not be persisted.")
return messages, None

View file

@ -140,3 +140,116 @@ def test_context_compaction_observability_uses_current_task_drive(monkeypatch, t
assert usage == {"prompt_tokens": 1}
assert seen["drive_root"] == tmp_path
assert seen["task_id"] == "task-42"
def test_emergency_compaction_necessity_uses_calibrated_density(monkeypatch, tmp_path):
"""NECESSITY is total calibrated pressure: the char budget compared in REAL
tokens via the main-loop density baseline (neutral 1.0 cold, measured
supersedes) on a measured ~1.7x-dense route the trigger fires before the
raw-char form would; on a cold store behavior is unchanged."""
from ouroboros import context_fit, loop
calls = []
monkeypatch.setattr(
loop, "_persist_compaction_checkpoint",
lambda m, **k: calls.append(("checkpoint", k["reason"])) or True,
)
monkeypatch.setattr(
loop, "compact_tool_history_llm",
lambda m, keep_recent, **k: (calls.append(("compact", keep_recent)) or (m, None)),
)
# Raw chars sit BELOW the 1.2M max trigger; ~1.7x measured density puts the
# calibrated real-token pressure over it.
monkeypatch.setattr(loop, "_estimate_messages_chars", lambda _m: 800_000)
def _ctx(density):
monkeypatch.setattr(context_fit, "main_loop_token_density", lambda _dr, _m: density)
return loop._CompactionRoundContext(
tools=SimpleNamespace(_ctx=SimpleNamespace(_pending_compaction=None, _accumulated_usage={})),
drive_root=tmp_path, drive_logs=tmp_path / "logs", task_id="task-cal",
round_idx=3, event_queue=None, active_use_local=False,
active_context_mode="max", checkpoint_injected=False,
emit_progress=lambda _msg: None,
)
messages = []
for i in range(8):
messages.append({
"role": "assistant", "content": f"r{i}",
"tool_calls": [{"id": f"c{i}", "function": {"name": "x", "arguments": "{}"}}],
})
messages.append({"role": "tool", "tool_call_id": f"c{i}", "content": "ok"})
loop._run_round_compaction(messages, _ctx(1.7))
assert ("checkpoint", "emergency_context_size") in calls
calls.clear()
loop._run_round_compaction(messages, _ctx(1.0)) # cold baseline: unchanged behavior
assert calls == []
def test_emergency_compaction_hysteresis_suppresses_futile_refire(monkeypatch, tmp_path):
"""UTILITY/rearm: a pass that could not get below the trigger arms a
hysteresis no per-round refire (no light-model call, no cache-destroying
rewrite) until the compactable region grows ~20% or N rounds pass. One loud
disclosure on arming."""
from ouroboros import context_fit, loop
from ouroboros.context_budget import COMPACTION_HYSTERESIS_ROUNDS
calls = []
progress = []
monkeypatch.setattr(
loop, "_persist_compaction_checkpoint",
lambda m, **k: calls.append("checkpoint") or True,
)
# FUTILE pass: returns the transcript unchanged.
monkeypatch.setattr(
loop, "compact_tool_history_llm",
lambda m, keep_recent, **k: (calls.append("compact") or (m, None)),
)
monkeypatch.setattr(context_fit, "main_loop_token_density", lambda _dr, _m: 1.0)
# Size scales with message count so the compactable region can grow.
monkeypatch.setattr(loop, "_estimate_messages_chars", lambda m: len(m) * 100_000)
def _messages(rounds):
out = []
for i in range(rounds):
out.append({
"role": "assistant", "content": f"r{i}",
"tool_calls": [{"id": f"h{i}", "function": {"name": "x", "arguments": "{}"}}],
})
out.append({"role": "tool", "tool_call_id": f"h{i}", "content": "ok"})
return out
state = {}
def _ctx(round_idx):
return loop._CompactionRoundContext(
tools=SimpleNamespace(_ctx=SimpleNamespace(_pending_compaction=None, _accumulated_usage=state)),
drive_root=tmp_path, drive_logs=tmp_path / "logs", task_id="task-hyst",
round_idx=round_idx, event_queue=None, active_use_local=False,
active_context_mode="max", checkpoint_injected=False,
emit_progress=progress.append,
)
# 10 rounds x 2 msgs x 100K = 2M chars > 1.2M: fires, futile, arms.
loop._run_round_compaction(_messages(10), _ctx(3))
assert calls == ["checkpoint", "compact"]
assert "_compaction_hysteresis" in state
assert any("futile" in p for p in progress)
# Same region, next round: suppressed (no checkpoint, no light-model call).
calls.clear()
progress.clear()
loop._run_round_compaction(_messages(10), _ctx(4))
assert calls == []
assert progress == [] # disclosed once, on arming — not per round
# Region grew >=20% (10 -> 13 rounds = 2.6M >= 2.4M): re-fires.
loop._run_round_compaction(_messages(13), _ctx(5))
assert calls == ["checkpoint", "compact"]
# Re-armed by the futile re-fire; N rounds later it re-fires on time alone.
calls.clear()
loop._run_round_compaction(_messages(13), _ctx(5 + COMPACTION_HYSTERESIS_ROUNDS))
assert calls == ["checkpoint", "compact"]