fix(agent): a compact asked for by hand no longer answers "too little added" after a checkpoint; asking waives how much was added, and an unchanged context is still declined

This commit is contained in:
yhh committed 2026-11-06 21:22:30 -08:00
1 parent 26f72585ee
commit 756c6159a6
4 files changed
+19 -19

No files matched your search

+3 -3
View File
@@ -609,9 +609,9 @@ func (a *Agent) SetSink(sink event.Sink) {
func (a *Agent) CompactRatio() float64 { return a.compactRatio }
// CompactNow runs one requested projection compaction (canonical transcript
// untouched). Asking waives the automatic threshold; it does not waive what the
// fold has to be worth, so a context nothing was added to is declined rather
// than summarized a second time. Only IgnoreEconomics waives that.
// untouched). Asking waives the automatic threshold and how much was added since
// the last checkpoint, but a context nothing was added to is still declined
// rather than summarized a second time. Only IgnoreEconomics waives that.
func (a *Agent) CompactNow(ctx context.Context, req CompactRequest) (CompactVerdict, error) {
prepared, err := a.window().contextManager().Prepare(ctx, ContextPreparePolicy{
Trigger: CompactionTriggerManual,
@@ -80,10 +80,10 @@ func TestManualCompactOnAnUnchangedContextBuysNothing(t *testing.T) {
}
}
// A checkpoint costs the whole prefix cache, so a second one waits for enough
// new closed history to pay for it — the same rule the automatic path uses,
// now applied to the path that actually fires in practice.
func TestManualCompactWaitsForEnoughNewHistory(t *testing.T) {
// Asking is the person deciding the fold is worth it: a few new turns after a
// checkpoint do not make /compact answer "too little", only an unchanged
// context does.
func TestManualCompactFoldsAfterALittleNewHistory(t *testing.T) {
sess := foldableSessionOverForce(60)
a, _ := manualAgent(t, sess)
if _, err := a.CompactNow(context.Background(), CompactRequest{}); err != nil {
@@ -91,19 +91,13 @@ func TestManualCompactWaitsForEnoughNewHistory(t *testing.T) {
}
first := archivedFolds(a)
addTurns(sess, 1, 5)
addTurns(sess, 12, 120)
verdict, err := a.CompactNow(context.Background(), CompactRequest{})
if err != nil {
t.Fatalf("CompactNow after a little more history: %v", err)
}
if archivedFolds(a) != first {
t.Errorf("a few hundred new tokens bought %d more folds", archivedFolds(a)-first)
}
if verdict.Compacted() || verdict.Reason == "" {
t.Errorf("verdict = %+v, want a declined one carrying its reason", verdict)
}
if CompactDeclineText(verdict.Reason) == "" {
t.Errorf("no words for reason %q", verdict.Reason)
if !verdict.Compacted() || archivedFolds(a)-first != 1 {
t.Errorf("verdict = %+v folds=%d, want one more fold when asked by hand", verdict, archivedFolds(a)-first)
}
}
+6 -2
View File
@@ -386,6 +386,10 @@ func compactionTelemetryFromSummary(trigger, cacheState string, sourceTokens int
type compactionScope struct {
ignoreThreshold bool
ignoreEconomics bool
// ignoreAmount waives only how much was added since the last checkpoint:
// a request by hand has decided the fold is worth it, but still declines a
// context that has not changed at all.
ignoreAmount bool
}
// compactToProjection installs one content-driven summary checkpoint:
@@ -423,7 +427,7 @@ func (a *contextWindow) compactToProjection(ctx context.Context, trigger, instru
// Every checkpoint costs the whole prefix cache, so a second one waits
// for a tail's worth of new closed history — otherwise a small window
// folds every few rounds and spends more than the fold frees.
if a.estimatedPromptTokens(msgs[held:start]) < a.refoldMinTokens() {
if !scope.ignoreAmount && a.estimatedPromptTokens(msgs[held:start]) < a.refoldMinTokens() {
return CompactionNoop, NoopFoldBelowEconomics, nil
}
}
@@ -431,7 +435,7 @@ func (a *contextWindow) compactToProjection(ctx context.Context, trigger, instru
// original stays whole for resume and rewind.
region := msgs[head:start]
kept, fold := a.partitionFold(region, msgs[start:])
if len(fold) == 0 || (!scope.ignoreEconomics && !a.foldEconomics(fold)) {
if len(fold) == 0 || (!scope.ignoreEconomics && !scope.ignoreAmount && !a.foldEconomics(fold)) {
return CompactionNoop, NoopFoldBelowEconomics, nil
}
reason, err := a.foldCannotFit(trigger, msgs, head, start)
+3 -1
View File
@@ -138,11 +138,13 @@ func (m ContextManager) prepareOnce(ctx context.Context, policy ContextPreparePo
return prepared, nil
}
// Asking, overflow and a physical ceiling each waive the trigger. Only the
// last two, and an explicit force, waive what the fold has to be worth.
// last two, and an explicit force, waive what the fold has to be worth; asking
// by hand waives only how much was added.
scope := compactionScope{
ignoreThreshold: policy.IgnoreThreshold || policy.Trigger == CompactionTriggerManual ||
policy.Trigger == CompactionTriggerOverflow || est >= hard,
ignoreEconomics: policy.IgnoreEconomics || policy.Trigger == CompactionTriggerOverflow || est >= hard,
ignoreAmount: policy.Trigger == CompactionTriggerManual,
}
if pressure < fold && !scope.ignoreThreshold {
return prepared, nil