feat(agent): add an economic context-maintenance boundary

Compaction was not broken. It faithfully executed a policy that only knew
one boundary: compact_ratio of the declared window. A provider preset
declaring a 1,000,000-token window therefore put the trigger at 850,000,
and a real session ran 2,150 rounds against a prompt that peaked at
321,265 — 37.8% of the budget, so maintenance was never once attempted.

    input       avg round latency
     86K        12.18s
    130K        15.16s
    158K        20.25s
    195K        28.17s
    226K        30.28s
    314K        37.91s

Capacity and economics are different boundaries. A model may support a 1M
context without making a 300K prompt economical to replay for hundreds of
agent rounds, so the automatic trigger is now whichever is reached first:
compact_ratio of the window, or context_soft_limit_tokens of visible input
(default 160000, negative disables). The verbatim tail follows the boundary
in force — a tail that reaches the trigger leaves nothing to fold, which
would have made the new boundary fire against a prompt it could not reduce.

A ratio placed past the window stays how automatic maintenance is turned
off: economics tightens a live boundary and never revives a retired one, so
an existing compact_ratio = 2 keeps meaning what it meant.

Every attempt now says what it decided. A no-fold verdict carries a code
rather than a sentence, because "already folded this turn" and "nothing
left to fold" read alike and mean opposite things about what the next round
can expect. The verdict reached while a turn is running was previously
returned silently, which is why the trajectory of that 15.6-hour session
cannot say whether maintenance was tried; it now emits once per turn per
reason, writes no receipt and blocks nothing.

This does not make long active turns repeatedly compactable. It makes the
first economic maintenance attempt observable; repeated same-turn
maintenance remains intentionally blocked, and
TestSecondMaintenanceInOneTurnReportsAlreadyCompacted is the regression
test that keeps that fact visible.

Three comments promising an adaptive stop that 21638e356 removed are
corrected to say what actually bounds a turn today: nothing, by default.

Baseline widened by three lines — the floor price of one config setting
(field, one-line comment, render line).
This commit is contained in:
esengine committed 2026-08-29 20:26:13 -07:00
1 parent ba415e863e
commit 280dfd8c92
22 files changed
+453 -69

No files matched your search

+18
View File
@@ -305,6 +305,23 @@ export interface ExtensionSurface {
view?: ExtensionView;
}
// One context-maintenance transaction. status "noop" carries code: the attempt
// ran and freed nothing, and code is what tells "already folded this turn" from
// "nothing left to fold" — they read alike and mean opposite things about what
// the next round can expect. boundary names which threshold was reached.
export interface ContextMaintenance {
status?: string;
action?: string;
trigger?: string;
code?: string;
boundary?: string;
triggerTokens?: number;
inputTokens?: number;
resultTokens?: number;
savedTokens?: number;
reason?: string;
}
export interface Compaction {
trigger?: string;
messages?: number;
@@ -466,6 +483,7 @@ export interface WireEvent {
guardian?: Guardian;
extension?: ExtensionSurface;
compaction?: Compaction;
maintenance?: ContextMaintenance;
streamAttempt?: StreamAttempt;
completion?: CompletionSummary;
graph?: GraphDelta;
+13 -2
View File
@@ -179,8 +179,19 @@ function record(ev: WireEvent): Made | null {
case "steer":
return { kind: "memory_recall", payload: [{ t: "steer delivered · " }, { b: ev.text ?? "" }] };
case "context_maintenance":
return { kind: "outcome_progress", payload: [{ t: ev.text || "context maintenance" }] };
case "context_maintenance": {
const m = ev.maintenance;
if (!m) return { kind: "outcome_progress", payload: [{ t: ev.text || "context maintenance" }] };
const payload: Span[] = [{ t: "context maintenance · " }, { b: m.status ?? "" }];
if (m.boundary) payload.push({ t: " · " }, { b: m.boundary });
// A verdict without its code is the line that made the last runaway
// unreadable: it says maintenance happened, never what it decided.
if (m.code) payload.push({ t: " · " }, { b: m.code });
if (m.inputTokens) payload.push({ t: " · in " }, { n: String(m.inputTokens) });
if (m.resultTokens) payload.push({ t: " → " }, { n: String(m.resultTokens) });
if (m.triggerTokens) payload.push({ t: " · trigger " }, { n: String(m.triggerTokens) });
return { kind: "outcome_progress", payload };
}
case "completion_summary": {
const c = ev.completion;
+3 -3
View File
@@ -954,9 +954,9 @@ func (a *Agent) reserveParentWrite(runTool tool.Tool, args json.RawMessage, read
// Run appends the user input and drives the tool loop until the model returns a
// final answer, the context is cancelled, or the provider errors. maxSteps <= 0
// leaves the loop unbounded here: bounding it is the host's call, and the
// adaptive stop is the no-progress ladder rather than a round count. Turn policy
// lives in beginRunTurn / runToolLoop / handleFinalResponse / handleToolRound.
// leaves the loop unbounded here: bounding it is the host's call, as a TaskBudget
// in hours or tokens rather than a round count, and every such budget ships off.
// Turn policy lives in beginRunTurn / runToolLoop / handleFinalResponse.
func (a *Agent) Run(ctx context.Context, input string) (runErr error) {
runMaxSteps := a.maxSteps
runMaxStepsKey := a.maxStepsKey
+4
View File
@@ -42,4 +42,8 @@ type CompactionBudgets struct {
UserTurnKeepTokens int
FirstTurnPinTokens int
CheckpointCeilingRatio float64
// ContextSoftLimitTokens is the economic maintenance boundary: the visible
// input size at which replaying the prompt each round stops being worth it,
// whatever window the provider declares. Negative disables that boundary.
ContextSoftLimitTokens int
}
+51 -17
View File
@@ -16,25 +16,26 @@ import (
)
// Compaction is a low-frequency cache-reset point: the prompt grows append-only
// until compactRatio of the window is crossed, then one content-driven summary
// until a maintenance boundary is crossed, then one content-driven summary
// checkpoint is installed (stable prefix + one structured digest + recent tail).
// 50% is only the normal acceptance ceiling — candidates are never padded up to it.
const (
defaultCompactRatio = 0.85 // sole automatic maintenance trigger (new configs)
checkpointCeilingRatio = 0.50 // normal auto-checkpoint acceptance ceiling
recentTailBudgetRatio = 0.10 // recent verbatim tail as a fraction of the window
minRecentTailTokens = 32 * 1024
maxRecentTailTokens = 96 * 1024
summaryOutputMaxTokens = 16 * 1024 // max digest output; further clipped by remaining candidate space
exceptionalMinSavingsRatio = 0.25 // when fixed prefix alone exceeds 50%, require at least this savings
minRecentKeep = 2 // never keep fewer recent messages than this
minCompactMessages = 2 // skip compaction below this many compactable messages
fallbackTokPerChar = 0.25 // ~4 chars/token, used before any usage is available to calibrate
defaultPinnedFirstUserTokens = 1500 // default ceiling on pinning the first user turn verbatim
pinnedFirstUserWindowFrac = 0.15 // and never pin a first turn worth more than this fraction of the window
keptUserTurnsWindowFrac = 0.05 // default verbatim user-turn budget, as a fraction of the window
keptUserTurnsFloorTokens = 1024 // ...with a floor so a small window still holds something
protocolReserveTokens = 256 // provider framing and control fields not represented by message estimates
defaultCompactRatio = 0.85 // capacity share of the window before maintenance
defaultContextSoftLimitTokens = 160_000 // economic maintenance boundary, independent of the window
checkpointCeilingRatio = 0.50 // normal auto-checkpoint acceptance ceiling
recentTailBudgetRatio = 0.10 // recent verbatim tail as a fraction of the window
minRecentTailTokens = 32 * 1024
maxRecentTailTokens = 96 * 1024
summaryOutputMaxTokens = 16 * 1024 // max digest output; further clipped by remaining candidate space
exceptionalMinSavingsRatio = 0.25 // when fixed prefix alone exceeds 50%, require at least this savings
minRecentKeep = 2 // never keep fewer recent messages than this
minCompactMessages = 2 // skip compaction below this many compactable messages
fallbackTokPerChar = 0.25 // ~4 chars/token, used before any usage is available to calibrate
defaultPinnedFirstUserTokens = 1500 // default ceiling on pinning the first user turn verbatim
pinnedFirstUserWindowFrac = 0.15 // and never pin a first turn worth more than this fraction of the window
keptUserTurnsWindowFrac = 0.05 // default verbatim user-turn budget, as a fraction of the window
keptUserTurnsFloorTokens = 1024 // ...with a floor so a small window still holds something
protocolReserveTokens = 256 // provider framing and control fields not represented by message estimates
)
var (
@@ -86,7 +87,8 @@ Rules: be terse — bullet points and fragments, not prose. Preserve identifiers
// budgets are intentionally absent: they are clipped against the final request
// at send time and must never make compaction happen earlier than the user's
// configured compact_ratio.
func (a *Agent) compactTrigger() int {
// capacityCompactTrigger answers how close a prompt is to not fitting.
func (a *Agent) capacityCompactTrigger() int {
window := a.effectiveContextWindow()
if window <= 0 {
return 0
@@ -101,6 +103,38 @@ func (a *Agent) compactTrigger() int {
return max(1, int(float64(window)*ratio))
}
// economicCompactTrigger answers what a prompt costs to replay, which is a
// property of its size and not of what the model could have held: a declared 1M
// window makes a 300k prompt legal, never economical. Negative disables it.
func (a *Agent) economicCompactTrigger() int {
if a.ablation.Off(ablation.Compaction) {
return 0
}
limit := a.budgets.ContextSoftLimitTokens
if limit < 0 {
return 0
}
if limit == 0 {
limit = defaultContextSoftLimitTokens
}
return limit
}
// compactTrigger is whichever boundary is reached first. An unmeasured window
// leaves both unset rather than guessing at a size, and a ratio placed past the
// window is how maintenance is turned off — economics tightens a live boundary,
// never revives a retired one.
func (a *Agent) compactTrigger() int {
capacity := a.capacityCompactTrigger()
if capacity <= 0 || a.compactRatio > 1 {
return capacity
}
if economic := a.economicCompactTrigger(); economic > 0 && economic < capacity {
return economic
}
return capacity
}
// hardInputCeiling is a physical input-safety boundary, not another user
// compaction threshold. Reply budgets are resolved independently at send time.
func (a *Agent) hardInputCeiling() int {
+1 -1
View File
@@ -144,7 +144,7 @@ func TestCompactionDoneCarriesWhatTheDigestKept(t *testing.T) {
a := New(&fakeProvider{reply: "## Files & code\n- internal/parser/lexer.go rewritten"}, reg, sess,
Options{ContextWindow: 60_000, CompactRatio: 0.5, RecentKeep: 2, ArchiveDir: testenv.TempDir(t)}, sink)
if _, err := a.compactToProjection(context.Background(), CompactionTriggerManual, "", true, false); err != nil {
if _, _, err := a.compactToProjection(context.Background(), CompactionTriggerManual, "", true, false); err != nil {
t.Fatalf("compactToProjection: %v", err)
}
if len(done) != 1 {
+27 -18
View File
@@ -380,7 +380,7 @@ func compactionTelemetryFromSummary(trigger, cacheState string, sourceTokens int
// compact writes a context projection; trigger stays "auto"/"manual" for UI cards.
func (a *Agent) compact(ctx context.Context, trigger, instructions string, force bool) error {
_, err := a.compactToProjection(ctx, trigger, instructions, force, false)
_, _, err := a.compactToProjection(ctx, trigger, instructions, force, false)
return err
}
@@ -389,12 +389,12 @@ func (a *Agent) compact(ctx context.Context, trigger, instructions string, force
// The canonical transcript is never rewritten. CompactionNoop means nothing
// was foldable; callers at physical overflow must treat that as hard failure.
// mustFree marks the fold the caller cannot proceed without.
func (a *Agent) compactToProjection(ctx context.Context, trigger, instructions string, force, mustFree bool) (CompactionOutcome, error) {
func (a *Agent) compactToProjection(ctx context.Context, trigger, instructions string, force, mustFree bool) (CompactionOutcome, CompactionNoopReason, error) {
a.sess.compactionRunMu.Lock()
defer a.sess.compactionRunMu.Unlock()
activeTurn := a.activeTurnCreatedAt.Load()
if activeTurn != 0 && a.sess.compaction.lastTurn.Load() == activeTurn && trigger != CompactionTriggerManual {
return CompactionNoop, nil
return CompactionNoop, NoopAlreadyCompactedThisTurn, nil
}
canonical, transcriptVersion := a.sess.conversation.snapshotMessagesVersion()
a.sess.compactionMu.Lock()
@@ -405,24 +405,24 @@ func (a *Agent) compactToProjection(ctx context.Context, trigger, instructions s
msgs := a.visibleInputForFold(stateSnapshot, canonical, transcriptVersion)
viewInputHash := providerVisibleFingerprint(provider.ModelMessages(msgs))
if trigger != CompactionTriggerManual && stateSnapshot.LastReceipt != nil && stateSnapshot.LastReceipt.Status == "applied" && stateSnapshot.LastReceipt.Action == "summary" && stateSnapshot.LastReceipt.InputHash == viewInputHash {
return CompactionNoop, nil
return CompactionNoop, NoopInputUnchanged, nil
}
head, start, ok := a.planFoldRegion(msgs, force)
head, start, ok, planReason := a.planFoldRegion(msgs, force)
if !ok {
return CompactionNoop, nil
return CompactionNoop, planReason, nil
}
// The annotation rides the projection, not the canonical transcript: the
// original stays whole for resume and rewind.
kept, fold, retention, policyKeep := a.partitionFoldForProjection(a.annotateFailureDiagnostics(ctx, msgs[head:start]))
if len(fold) == 0 || (!force && !a.foldEconomics(fold)) {
return CompactionNoop, nil
return CompactionNoop, NoopFoldBelowEconomics, nil
}
fold, priorIndex := stripFoldIndexFromDigests(fold)
foldIndex := buildFoldIndex(msgs[head:start], policyKeep, a.toolFactsFor,
a.canonicalOriginFor(stateSnapshot, canonical, msgs, head))
fixedPrefixTokens := a.estimatedPromptTokens(a.providerProjectionMessages(msgs[:head]))
if a.effectiveContextWindow() > 0 && fixedPrefixTokens >= a.compactTrigger() {
return CompactionNoop, rejectCheckpoint("fixed prefix (%d tokens) already exceeds trigger (%d)", fixedPrefixTokens, a.compactTrigger())
return CompactionNoop, NoopFixedPrefixAboveTrigger, rejectCheckpoint("fixed prefix (%d tokens) already exceeds trigger (%d)", fixedPrefixTokens, a.compactTrigger())
}
sourceTokens := a.estimatedPromptTokens(msgs)
@@ -441,18 +441,18 @@ func (a *Agent) compactToProjection(ctx context.Context, trigger, instructions s
fold, instructions, err = a.interceptCompactionPrepare(ctx, fold, instructions)
if err != nil {
a.emitCompactionAborted(trigger)
return CompactionNoop, err
return CompactionNoop, "", err
}
if len(fold) == 0 {
a.emitCompactionAborted(trigger)
return CompactionNoop, nil
return CompactionNoop, NoopFoldEmptyAfterHooks, nil
}
res, tele, err := a.foldOrDegrade(ctx, trigger, mustFree, fold, instructions, sourceTokens)
if err != nil {
a.emitCompactionTelemetry(tele)
a.emitCompactionAborted(trigger)
return CompactionNoop, err
return CompactionNoop, "", err
}
res.Text = a.attachFoldIndex(res.Text, priorIndex, foldIndex)
summary, err := a.interceptCompactionComplete(ctx, res.Text)
@@ -460,7 +460,7 @@ func (a *Agent) compactToProjection(ctx context.Context, trigger, instructions s
tele.Error = err.Error()
a.emitCompactionTelemetry(tele)
a.emitCompactionAborted(trigger)
return CompactionNoop, err
return CompactionNoop, "", err
}
projMsgs := checkpointProjectionMessages(msgs, head, start, kept, summary)
@@ -471,7 +471,7 @@ func (a *Agent) compactToProjection(ctx context.Context, trigger, instructions s
a.emitCompactionTelemetry(tele)
if err := a.acceptCheckpointCandidate(trigger, force, sourceTokens, projTokens, fixedPrefixTokens); err != nil {
a.emitCompactionAborted(trigger)
return CompactionNoop, err
return CompactionNoop, "", err
}
viewOutputHash := providerVisibleFingerprint(provider.ModelMessages(projMsgs))
_, err = a.commitSummaryProjection(summaryProjectionCommit{
@@ -483,7 +483,7 @@ func (a *Agent) compactToProjection(ctx context.Context, trigger, instructions s
})
if err != nil {
a.emitCompactionAborted(trigger)
return CompactionNoop, err
return CompactionNoop, "", err
}
a.svc.sink.Emit(event.Event{Kind: event.CompactionDone, Compaction: event.Compaction{
Trigger: trigger, Messages: len(fold), Summary: summary,
@@ -495,7 +495,7 @@ func (a *Agent) compactToProjection(ctx context.Context, trigger, instructions s
// Only once the checkpoint is committed: a rejected candidate folded nothing.
a.sess.compaction.lastUserTurns = retention
a.noticeDroppedUserTurns(retention)
return CompactionInstalled, nil
return CompactionInstalled, "", nil
}
// visibleInputForFold prefers the prior projection + new history over full canonical.
@@ -563,18 +563,27 @@ func (a *Agent) acceptCheckpointCandidate(trigger string, force bool, sourceToke
}
// planFoldRegion returns [head:start] to fold; force shrinks the recent tail.
func (a *Agent) planFoldRegion(msgs []provider.Message, force bool) (head, start int, ok bool) {
func (a *Agent) planFoldRegion(msgs []provider.Message, force bool) (head, start int, ok bool, reason CompactionNoopReason) {
head, start, ok = a.planCompaction(msgs, minCompactMessages, force)
if !ok {
head, start, ok = a.planCompaction(msgs, 1, force)
}
if !ok {
return head, start, false
return head, start, false, NoopNoFoldableRegion
}
// The running turn stays verbatim, so its start is where the fold has to
// end. A turn long enough to reach the trigger on its own leaves nothing
// behind it, and that is a different verdict from having nothing to fold.
if active := a.activeTurnStart(msgs); active >= head && active < start {
start = active
if start <= head {
return head, start, false, NoopActiveTurnBoundary
}
}
return head, start, start > head
if start <= head {
return head, start, false, NoopNoFoldableRegion
}
return head, start, true, ""
}
func (a *Agent) partitionFoldForProjection(region []provider.Message) (kept, fold []provider.Message, retention userTurnRetention, policyKeep []bool) {
@@ -67,7 +67,7 @@ func prepareContext(ctx context.Context, a *Agent, trigger string) error {
func foldRegionOf(a *Agent) []provider.Message {
canonical, version := a.sess.conversation.snapshotMessagesVersion()
msgs := a.visibleInputForFold(a.sess.compactionState, canonical, version)
head, start, ok := a.planFoldRegion(msgs, false)
head, start, ok, _ := a.planFoldRegion(msgs, false)
if !ok {
return nil
}
+17 -3
View File
@@ -37,15 +37,28 @@ func TestRecentTailBudgetClamp(t *testing.T) {
{2_000_000, maxRecentTailTokens}, // still 96K
}
for _, tc := range cases {
a := &Agent{agentConfig: agentConfig{contextWindow: tc.window, compactRatio: defaultCompactRatio}}
a := &Agent{agentConfig: agentConfig{contextWindow: tc.window, compactRatio: defaultCompactRatio,
budgets: CompactionBudgets{ContextSoftLimitTokens: -1}}}
if got := a.recentTailBudget(); got != tc.want {
t.Fatalf("window %d: recentTailBudget = %d, want %d", tc.window, got, tc.want)
}
}
}
// A tail reaching the trigger leaves nothing to fold, so the tail follows
// whichever boundary is in force — including the one that does not come from
// the window. Without this the economic boundary would arrive with the whole
// prompt still held verbatim.
func TestRecentTailFollowsTheEconomicBoundary(t *testing.T) {
a := &Agent{agentConfig: agentConfig{contextWindow: 1_000_000, compactRatio: defaultCompactRatio}}
if got, want := a.recentTailBudget(), defaultContextSoftLimitTokens/2; got != want {
t.Fatalf("recentTailBudget = %d, want %d (half the economic trigger)", got, want)
}
}
func TestCheckpointCeilingAndExceptionalSavings(t *testing.T) {
a := &Agent{agentConfig: agentConfig{contextWindow: 1_000_000, compactRatio: 0.85}}
a := &Agent{agentConfig: agentConfig{contextWindow: 1_000_000, compactRatio: 0.85,
budgets: CompactionBudgets{ContextSoftLimitTokens: -1}}}
if got := a.checkpointCeiling(); got != 500_000 {
t.Fatalf("checkpointCeiling = %d, want 500000", got)
}
@@ -58,7 +71,8 @@ func TestCheckpointCeilingAndExceptionalSavings(t *testing.T) {
}
func TestAcceptCheckpointCandidateRules(t *testing.T) {
a := &Agent{agentConfig: agentConfig{contextWindow: 1_000_000, compactRatio: 0.85}}
a := &Agent{agentConfig: agentConfig{contextWindow: 1_000_000, compactRatio: 0.85,
budgets: CompactionBudgets{ContextSoftLimitTokens: -1}}}
// 20% candidate under normal path: accept.
if err := a.acceptCheckpointCandidate(CompactionTriggerPressure, false, 850_000, 180_000, 50_000); err != nil {
t.Fatalf("20%% candidate: %v", err)
+1 -1
View File
@@ -70,7 +70,7 @@ func TestLargeFixedPrefixDoesNotRefuseCompaction(t *testing.T) {
t.Fatalf("fixture prefix is %d characters, under the %d trigger; it cannot show the bug", len(prefix), a.compactTrigger())
}
if _, err := a.compactToProjection(context.Background(), CompactionTriggerManual, "", true, false); err != nil {
if _, _, err := a.compactToProjection(context.Background(), CompactionTriggerManual, "", true, false); err != nil {
t.Fatalf("compaction refused a prefix that fits: %v", err)
}
}
+1 -1
View File
@@ -333,7 +333,7 @@ func TestRangeCompressionSharesSummarySingleflight(t *testing.T) {
a := New(prov, tool.NewRegistry(), sess, Options{RecentKeep: 2, ArchiveDir: testenv.TempDir(t)}, event.Discard)
autoErr := make(chan error, 1)
go func() {
_, err := a.compactToProjection(context.Background(), CompactionTriggerPressure, "", true, false)
_, _, err := a.compactToProjection(context.Background(), CompactionTriggerPressure, "", true, false)
autoErr <- err
}()
<-prov.started
@@ -40,13 +40,15 @@ func largeRestartToolHistory(results int) []provider.Message {
func newRestartMaintenanceAgent(path string, messages []provider.Message, sink event.Sink) *Agent {
// Explicit 0.80 matches the v1.22.0 user scenario and the plan's regression
// fixture. New configs default to 0.85; users who set 0.80 keep 0.80.
// Economics off: the capacity share is the only trigger under test here.
return New(nil, tool.NewRegistry(), &Session{Messages: append([]provider.Message(nil), messages...)}, Options{
ContextWindow: restartMaintenanceWindow,
CompactRatio: 0.80,
RecentKeep: 2,
SessionPath: path,
WorkspaceID: "workspace",
ModelRef: "provider/model",
ContextWindow: restartMaintenanceWindow,
CompactRatio: 0.80,
RecentKeep: 2,
SessionPath: path,
WorkspaceID: "workspace",
ModelRef: "provider/model",
CompactionBudgets: CompactionBudgets{ContextSoftLimitTokens: -1},
}, sink)
}
+20 -5
View File
@@ -19,12 +19,23 @@ type compactionProgress struct {
// lastTurn stops the post-turn observer and the pre-send preflight from
// paying for two summaries during one active tool loop.
lastTurn atomic.Int64
// lastNoop is the verdict already reported for the running turn. A crossed
// threshold stays crossed, so without it every later round of the same turn
// would report the same "folded nothing" again.
lastNoop maintenanceNoop
// lastUserTurns is what the most recent fold could and could not hold of
// the user's own words. The notice fires once, at the moment of the fold;
// this is what /context can still answer with afterwards.
lastUserTurns userTurnRetention
}
// maintenanceNoop is one reported no-fold verdict, scoped to the turn it was
// reached under: the same reason in a later turn is news again.
type maintenanceNoop struct {
reason CompactionNoopReason
turn int64
}
// ContextManager is the sole owner of provider-visible context maintenance.
// Canonical session messages are immutable inputs; Prepare evolves only the
// durable projection and returns the exact visible view for one sampling round.
@@ -122,7 +133,7 @@ func (m ContextManager) foldContext(ctx context.Context, prepared PreparedContex
// Where this function would answer ErrCompactionRequired, the fold is the
// only way out and a failed summary must degrade rather than strand the turn.
mustFree := policy.Trigger != CompactionTriggerManual && (policy.Trigger == CompactionTriggerOverflow || est >= hard)
outcome, err := a.compactToProjection(ctx, policy.Trigger, policy.Instructions, forceFold, mustFree)
outcome, noopReason, err := a.compactToProjection(ctx, policy.Trigger, policy.Instructions, forceFold, mustFree)
if err != nil {
// Cancellation is the caller's decision, not a summary that failed:
// recording it as one would blame the summarizer for this generation,
@@ -134,7 +145,7 @@ func (m ContextManager) foldContext(ctx context.Context, prepared PreparedContex
// Transcript changed during the summary call: discard the candidate
// and block this generation so we do not pay for a second summary.
reason := "context changed during summary; automatic retry blocked for this generation"
a.recordContextMaintenanceBlocked(inputHash, policy.Trigger, "summary", reason)
a.recordContextMaintenanceBlocked(inputHash, policy.Trigger, "summary", "", reason)
if policy.Trigger == CompactionTriggerOverflow || est >= hard {
return PreparedContext{}, fmt.Errorf("%w: %s", ErrCompactionRequired, reason)
}
@@ -145,7 +156,7 @@ func (m ContextManager) foldContext(ctx context.Context, prepared PreparedContex
status = "blocked"
}
reason := fmt.Sprintf("context summary failed: %v", err)
a.recordContextMaintenanceOutcome(inputHash, policy.Trigger, "summary", status, reason)
a.recordContextMaintenanceOutcome(inputHash, policy.Trigger, "summary", status, "", reason)
if policy.Trigger == CompactionTriggerManual {
return PreparedContext{}, err
}
@@ -163,13 +174,17 @@ func (m ContextManager) foldContext(ctx context.Context, prepared PreparedContex
if outcome == CompactionNoop {
canonical, _ := a.sess.conversation.snapshotMessagesVersion()
if policy.Trigger == CompactionTriggerPressure && a.activeTurnStart(canonical) >= 0 {
// A running turn may still grow past a threshold it cannot fold
// below. Saying so is the only record that the attempt happened:
// this path deliberately does not block the next one.
a.noteMaintenanceNoop(policy.Trigger, noopReason, est)
return prepared, nil
}
reason := "context is above the maintenance threshold but no foldable region remains"
if policy.Trigger == CompactionTriggerManual {
reason = "no foldable region remains"
}
a.recordContextMaintenanceBlocked(inputHash, policy.Trigger, "summary", reason)
a.recordContextMaintenanceBlocked(inputHash, policy.Trigger, "summary", noopReason, reason)
// Manual carries Force, so without this a hand-typed compact answered
// with the overflow error — a sentence about a provider limit nowhere
// near being hit. Nothing to fold is a verdict, not a failure.
@@ -188,7 +203,7 @@ func (m ContextManager) foldContext(ctx context.Context, prepared PreparedContex
}
if result.InputTokens >= fold {
reason := fmt.Sprintf("summary result remains above fold trigger (%d >= %d)", result.InputTokens, fold)
a.recordContextMaintenanceBlocked(a.contextMaintenanceInputHash(result.Messages), policy.Trigger, "summary", reason)
a.recordContextMaintenanceBlocked(a.contextMaintenanceInputHash(result.Messages), policy.Trigger, "summary", "", reason)
a.sess.compaction.stuck = true
if policy.Trigger == CompactionTriggerOverflow || result.InputTokens >= hard {
return PreparedContext{}, fmt.Errorf("%w: %s", ErrCompactionRequired, reason)
+38 -3
View File
@@ -53,18 +53,52 @@ func (a *Agent) emitContextMaintenance(r *ContextMaintenanceReceipt) {
InputTokens: r.InputTokens, ResultTokens: r.ResultTokens, SavedTokens: r.SavedTokens,
AffectedToolResults: r.AffectedToolResults, ProjectionVersion: r.ProjectionVersion,
CacheBreak: r.CacheBreak, Reason: r.Reason,
Code: r.Code, Boundary: r.Boundary, TriggerTokens: r.TriggerTokens,
}})
}
// compactBoundary names the threshold that decides maintenance right now: the
// window share, or the absolute visible-input size that does not come from it.
func (a *Agent) compactBoundary() string {
capacity := a.capacityCompactTrigger()
if capacity <= 0 || a.compactRatio > 1 {
return ""
}
if economic := a.economicCompactTrigger(); economic > 0 && economic < capacity {
return "economic"
}
return "capacity"
}
// noteMaintenanceNoop reports one attempt that folded nothing. It writes no
// receipt and advances no generation: nothing was spent and nothing is blocked,
// so the next round is free to try again — which is exactly why the attempt
// would otherwise leave no trace at all.
func (a *Agent) noteMaintenanceNoop(trigger string, reason CompactionNoopReason, inputTokens int) {
if a == nil || reason == "" || a.svc.sink == nil {
return
}
turn := a.activeTurnCreatedAt.Load()
if a.sess.compaction.lastNoop.reason == reason && a.sess.compaction.lastNoop.turn == turn {
return
}
a.sess.compaction.lastNoop = maintenanceNoop{reason: reason, turn: turn}
a.svc.sink.Emit(event.Event{Kind: event.ContextMaintenanceEvent, Maintenance: &event.ContextMaintenance{
Status: "noop", Action: "summary", Trigger: trigger, Code: string(reason),
Boundary: a.compactBoundary(), TriggerTokens: a.compactTrigger(), InputTokens: inputTokens,
}})
}
// recordContextMaintenanceBlocked persists a generation-scoped blocked receipt.
func (a *Agent) recordContextMaintenanceBlocked(inputHash, trigger, action, reason string) {
a.recordContextMaintenanceOutcome(inputHash, trigger, action, "blocked", reason)
// code is empty where the verdict has no identity beyond its sentence.
func (a *Agent) recordContextMaintenanceBlocked(inputHash, trigger, action string, code CompactionNoopReason, reason string) {
a.recordContextMaintenanceOutcome(inputHash, trigger, action, "blocked", code, reason)
}
// recordContextMaintenanceOutcome records blocked or failed for the current
// generation. Automatic Prepare will not re-enter summary until the generation
// advances (successful install, manual compress, or lineage change).
func (a *Agent) recordContextMaintenanceOutcome(inputHash, trigger, action, status, reason string) {
func (a *Agent) recordContextMaintenanceOutcome(inputHash, trigger, action, status string, code CompactionNoopReason, reason string) {
if a == nil || a.sess.conversation == nil {
return
}
@@ -110,6 +144,7 @@ func (a *Agent) recordContextMaintenanceOutcome(inputHash, trigger, action, stat
Trigger: trigger, SourceProjection: state.Projection.ProjectionVersion,
ProjectionVersion: state.Projection.ProjectionVersion, InputHash: inputHash,
BlockedInputHash: inputHash, Reason: reason, CreatedAt: now,
Code: string(code), Boundary: a.compactBoundary(), TriggerTokens: a.compactTrigger(),
}
state.UpdatedAt = now
a.sess.compactionState = state
+214
View File
@@ -0,0 +1,214 @@
package agent
import (
"context"
"strings"
"testing"
"reasonix/internal/event"
"reasonix/internal/provider"
"reasonix/internal/testenv"
"reasonix/internal/tool"
)
// Capacity and economics answer different questions: how close the prompt is to
// not fitting, and how much it costs to replay. A declared 1M window makes a
// 300K prompt legal without making it economical, so the boundary that decides
// maintenance is whichever one is reached first.
func TestMaintenanceBoundaryIsTheNearerOfCapacityAndEconomics(t *testing.T) {
for _, tc := range []struct {
name string
window int
ratio float64
soft int
trigger int
boundary string
}{
{"million-window folds on economics", 1_000_000, 0.85, 0, defaultContextSoftLimitTokens, "economic"},
{"small window keeps capacity first", 128_000, 0.85, 0, 108_800, "capacity"},
{"configured soft limit wins when lower", 1_000_000, 0.85, 96_000, 96_000, "economic"},
{"configured soft limit loses when higher", 200_000, 0.85, 400_000, 170_000, "capacity"},
{"negative soft limit disables economics", 1_000_000, 0.85, -1, 850_000, "capacity"},
{"no declared window leaves both unset", 0, 0.85, 0, 0, ""},
// A ratio past the window is how a user turns automatic maintenance off.
{"ratio past the window stays off", 1_000_000, 2, 0, 2_000_000, ""},
} {
t.Run(tc.name, func(t *testing.T) {
a := New(nil, tool.NewRegistry(), &Session{}, Options{
ContextWindow: tc.window,
CompactRatio: tc.ratio,
CompactionBudgets: CompactionBudgets{ContextSoftLimitTokens: tc.soft},
}, event.Discard)
if got := a.compactTrigger(); got != tc.trigger {
t.Errorf("compactTrigger = %d, want %d", got, tc.trigger)
}
if got := a.compactBoundary(); got != tc.boundary {
t.Errorf("compactBoundary = %q, want %q", got, tc.boundary)
}
})
}
}
// economicFixture is a session whose visible input sits far above the economic
// boundary and far below the capacity one, which is exactly the shape that ran
// for 2150 rounds at 321K tokens inside a declared 1M window.
const economicActiveTurnAt int64 = 1_700_000_000_000
func economicFixture(t *testing.T, soft int, activeTurnRound int) (*Agent, *recordSink) {
t.Helper()
sess := &Session{Messages: []provider.Message{
{Role: provider.RoleSystem, Content: "system"},
{Role: provider.RoleUser, Content: "long-running task"},
}}
body := strings.Repeat("x", 24*1024)
for i := range 40 {
turn := provider.Message{Role: provider.RoleUser, Content: body}
if i == activeTurnRound {
turn.CreatedAt = economicActiveTurnAt
}
sess.Messages = append(sess.Messages,
provider.Message{Role: provider.RoleAssistant, Content: "step " + string(rune('a'+i%26))},
turn)
}
sink := &recordSink{}
a := New(&fakeProvider{reply: "digest"}, tool.NewRegistry(), sess, Options{
ContextWindow: 1_000_000,
CompactRatio: 0.85,
RecentKeep: 2,
ArchiveDir: testenv.TempDir(t),
CompactionBudgets: CompactionBudgets{ContextSoftLimitTokens: soft},
}, sink)
return a, sink
}
// A prompt above the economic boundary must be maintained even though it is
// nowhere near the window share, and one below it must not be.
func TestEconomicTriggerFoldsBelowCapacityShare(t *testing.T) {
a, sink := economicFixture(t, 0, -1)
visible := a.estimatedVisibleRequestTokens(a.modelVisibleMessages())
if visible <= a.economicCompactTrigger() || visible >= a.capacityCompactTrigger() {
t.Fatalf("fixture input %d is not between the economic (%d) and capacity (%d) boundaries",
visible, a.economicCompactTrigger(), a.capacityCompactTrigger())
}
if _, err := a.contextManager().Prepare(context.Background(), ContextPreparePolicy{Trigger: CompactionTriggerPressure}); err != nil {
t.Fatal(err)
}
if got := a.currentProjectionVersion(); got == 0 {
t.Fatal("input above the economic boundary installed no projection")
}
if got := appliedMaintenanceEvents(sink); got != 1 {
t.Fatalf("applied maintenance events = %d, want 1", got)
}
}
func TestInputBelowBothBoundariesIsNotMaintained(t *testing.T) {
a, sink := economicFixture(t, 4_000_000, -1)
if _, err := a.contextManager().Prepare(context.Background(), ContextPreparePolicy{Trigger: CompactionTriggerPressure}); err != nil {
t.Fatal(err)
}
if got := a.currentProjectionVersion(); got != 0 {
t.Fatalf("input below both boundaries installed projection version %d", got)
}
if got := appliedMaintenanceEvents(sink); got != 0 {
t.Fatalf("applied maintenance events = %d, want 0", got)
}
}
// growTurn appends rounds to the running turn, which is what a tool loop does
// between two Prepare calls and what keeps its history unfoldable.
func growTurn(a *Agent, rounds int) {
body := strings.Repeat("x", 24*1024)
for i := range rounds {
a.sess.conversation.Add(provider.Message{Role: provider.RoleAssistant, Content: "more " + string(rune('a'+i%26))})
a.sess.conversation.Add(provider.Message{Role: provider.RoleTool, ToolCallID: "t", Name: "read_file", Content: body})
}
}
func maintenanceCodes(sink *recordSink, status string) []string {
var codes []string
for _, got := range sink.kinds(event.ContextMaintenanceEvent) {
if got.Maintenance != nil && got.Maintenance.Status == status {
codes = append(codes, got.Maintenance.Code)
}
}
return codes
}
// A turn folds at most once, and the rounds after it keep arriving above the
// boundary. Without a verdict for those rounds the trajectory shows a session
// that never tried to maintain its context, which is what this whole path was
// missing: the attempt happened and freed nothing.
func TestSecondMaintenanceInOneTurnReportsAlreadyCompacted(t *testing.T) {
// The running turn starts late enough that the rounds before it fold, so
// the first attempt succeeds and the turn is marked as already compacted.
a, sink := economicFixture(t, 0, 30)
a.activeTurnCreatedAt.Store(economicActiveTurnAt)
ctx := context.Background()
if _, err := a.contextManager().Prepare(ctx, ContextPreparePolicy{Trigger: CompactionTriggerPressure}); err != nil {
t.Fatal(err)
}
if a.currentProjectionVersion() == 0 {
t.Fatal("first attempt installed no projection; the fixture cannot show the second")
}
// The turn keeps running. Its own rounds are held verbatim, so the visible
// input climbs back over the boundary with nothing new left to fold.
growTurn(a, 30)
if _, err := a.contextManager().Prepare(ctx, ContextPreparePolicy{Trigger: CompactionTriggerPressure}); err != nil {
t.Fatal(err)
}
codes := maintenanceCodes(sink, "noop")
if len(codes) != 1 || codes[0] != string(NoopAlreadyCompactedThisTurn) {
t.Fatalf("noop codes = %v, want exactly [%s]", codes, NoopAlreadyCompactedThisTurn)
}
}
// The threshold stays crossed for the rest of the turn, so the same verdict is
// reached on every later round. Reporting it once is what keeps a trajectory
// readable; reporting it never is what made this invisible.
func TestRepeatedNoopInOneTurnReportsOnce(t *testing.T) {
a, sink := economicFixture(t, 0, 30)
a.activeTurnCreatedAt.Store(economicActiveTurnAt)
ctx := context.Background()
if _, err := a.contextManager().Prepare(ctx, ContextPreparePolicy{Trigger: CompactionTriggerPressure}); err != nil {
t.Fatal(err)
}
for range 4 {
growTurn(a, 10)
if _, err := a.contextManager().Prepare(ctx, ContextPreparePolicy{Trigger: CompactionTriggerPressure}); err != nil {
t.Fatal(err)
}
}
if codes := maintenanceCodes(sink, "noop"); len(codes) != 1 {
t.Fatalf("noop codes = %v, want exactly one report for the turn", codes)
}
}
// A turn that is the whole transcript has nothing behind it to fold. That is a
// different verdict from an exhausted history, and the two are told apart by
// the code rather than by reading the sentence beside it.
func TestActiveTurnBoundaryLeavesNothingToFold(t *testing.T) {
body := strings.Repeat("x", 24*1024)
// Too large to be pinned as a brief, so the fixed prefix is the system
// message alone and the running turn starts exactly where the fold would.
sess := &Session{Messages: []provider.Message{
{Role: provider.RoleSystem, Content: "system"},
{Role: provider.RoleUser, Content: body, CreatedAt: economicActiveTurnAt},
}}
for i := range 40 {
sess.Messages = append(sess.Messages,
provider.Message{Role: provider.RoleAssistant, Content: "step " + string(rune('a'+i%26))},
provider.Message{Role: provider.RoleTool, ToolCallID: "t", Name: "read_file", Content: body})
}
sink := &recordSink{}
a := New(&fakeProvider{reply: "digest"}, tool.NewRegistry(), sess, Options{
ContextWindow: 1_000_000, CompactRatio: 0.85, RecentKeep: 2, ArchiveDir: testenv.TempDir(t),
}, sink)
a.activeTurnCreatedAt.Store(economicActiveTurnAt)
if _, err := a.contextManager().Prepare(context.Background(), ContextPreparePolicy{Trigger: CompactionTriggerPressure}); err != nil {
t.Fatal(err)
}
codes := maintenanceCodes(sink, "noop")
if len(codes) != 1 || codes[0] != string(NoopActiveTurnBoundary) {
t.Fatalf("noop codes = %v, want exactly [%s]", codes, NoopActiveTurnBoundary)
}
}
+18
View File
@@ -92,6 +92,9 @@ type ContextMaintenanceReceipt struct {
SummaryHash string `json:"summary_hash,omitempty"`
CacheBreak bool `json:"cache_break,omitempty"`
Reason string `json:"reason,omitempty"`
Code string `json:"code,omitempty"`
Boundary string `json:"boundary,omitempty"`
TriggerTokens int `json:"trigger_tokens,omitempty"`
BlockedInputHash string `json:"blocked_input_hash,omitempty"`
CreatedAt time.Time `json:"created_at,omitempty"`
}
@@ -106,6 +109,21 @@ const (
CompactionNoop
)
// CompactionNoopReason identifies why an attempt folded nothing. "Already folded
// this turn" and "nothing left to fold" read alike and mean opposite things about
// what the next round can expect, so the verdict carries a code, not a sentence.
type CompactionNoopReason string
const (
NoopAlreadyCompactedThisTurn CompactionNoopReason = "already_compacted_this_turn"
NoopActiveTurnBoundary CompactionNoopReason = "active_turn_boundary"
NoopNoFoldableRegion CompactionNoopReason = "no_foldable_region"
NoopFoldBelowEconomics CompactionNoopReason = "fold_below_economics"
NoopInputUnchanged CompactionNoopReason = "input_unchanged"
NoopFoldEmptyAfterHooks CompactionNoopReason = "fold_empty_after_hooks"
NoopFixedPrefixAboveTrigger CompactionNoopReason = "fixed_prefix_above_trigger"
)
// RecallLedger is what one projection generation has already pulled back out
// of the fold. Carrying the generation it belongs to is what resets the budget:
// a stale generation reads as an unspent one, with no reset call to forget.
+1
View File
@@ -16,5 +16,6 @@ func compactionBudgets(cfg *config.Config) agent.CompactionBudgets {
UserTurnKeepTokens: cfg.Agent.UserTurnKeepTokens,
FirstTurnPinTokens: cfg.Agent.FirstTurnPinTokens,
CheckpointCeilingRatio: cfg.Agent.CheckpointCeilingRatio,
ContextSoftLimitTokens: cfg.Agent.ContextSoftLimitTokens,
}
}
+4 -2
View File
@@ -1321,6 +1321,8 @@ type AgentConfig struct {
// CheckpointCeilingRatio is how small an automatic fold's result must be,
// as a fraction of the window, before it is accepted. 0 selects 0.50.
CheckpointCeilingRatio float64 `toml:"checkpoint_ceiling_ratio"`
// Folds on input size, not window share: 1M declared makes 300k legal, never economical.
ContextSoftLimitTokens int `toml:"context_soft_limit_tokens"`
// ColdResumePrune elides stale tool results when a session reopens past the
// provider cache window. nil = default enabled.
ColdResumePrune *bool `toml:"cold_resume_prune"`
@@ -1775,8 +1777,8 @@ func Default() *Config {
},
Agent: AgentConfig{
SystemPrompt: DefaultSystemPrompt,
// Normal interactive execution has no configurable total round cap. It
// is bounded by adaptive progress guards and context compaction instead.
// No total round cap, and nothing else bounds a turn by default: the
// task budgets below ship off and the round guards were retired.
MaxSteps: 0,
PlannerMaxSteps: 0,
AutoPlan: "off",
+1
View File
@@ -207,6 +207,7 @@ func RenderTOMLForScope(c *Config, scope RenderScope) string {
} else {
b.WriteString("# checkpoint_ceiling_ratio = 0.50 # how small a fold's result must be to be accepted\n")
}
fmt.Fprintf(&b, "context_soft_limit_tokens = %d # fold at this visible input size whatever the window says; 0 = 160000, negative = off\n", c.Agent.ContextSoftLimitTokens)
if c.Agent.RecentKeep > 0 {
fmt.Fprintf(&b, "recent_keep = %d # minimum recent messages kept verbatim\n", c.Agent.RecentKeep)
} else {
+6
View File
@@ -401,6 +401,12 @@ type ContextMaintenance struct {
ProjectionVersion uint64 `json:"projectionVersion,omitempty"`
CacheBreak bool `json:"cacheBreak,omitempty"`
Reason string `json:"reason,omitempty"`
// Code identifies a no-fold verdict for a reader that must tell them apart;
// Reason is the same verdict as a sentence. Boundary names which threshold
// was reached (capacity or economic) and TriggerTokens is its size.
Code string `json:"code,omitempty"`
Boundary string `json:"boundary,omitempty"`
TriggerTokens int `json:"triggerTokens,omitempty"`
}
// GuardianResult carries the outcome of a guardian sub-agent safety review.
+1 -1
View File
@@ -46,7 +46,7 @@ func NewProgressTracker() *ProgressTracker {
// round's evidence gain. Reading something new is progress only while it leads
// somewhere: past explorationRunLimit look-only rounds the novelty stops
// counting, because a loop that keeps opening files it has never opened scored
// positive every round and so could never reach the no-progress ladder.
// positive every round. No guard consumes this today.
func (t *ProgressTracker) ScoreRound(receipts []Receipt) int {
if t == nil {
return 0
+5 -5
View File
@@ -6,9 +6,9 @@
"complexity": 1513,
"error-text": 12,
"essay": 3245,
"file-size": 41730,
"file-size": 41733,
"frontend-parity": 150,
"function-size": 6595,
"function-size": 6596,
"layering": 0,
"marker": 0,
"narrative": 57,
@@ -626,7 +626,7 @@
},
"internal/config/config.go": {
"essay": 50,
"file-size": 1436,
"file-size": 1438,
"narrative": 2
},
"internal/config/credentials.go": {
@@ -704,8 +704,8 @@
"internal/config/render.go": {
"complexity": 226,
"essay": 2,
"file-size": 923,
"function-size": 857,
"file-size": 924,
"function-size": 858,
"orphan": 1
},
"internal/config/vision.go": {