fix(explore): fund the guard from room that exists, and cut the epilogue first (CG-31)

Two corrections found by measuring the first cut of the guard against the
6-repo suite. The first version held back the FULL sum of the reservations
below a file. On django that took 2,319 chars off a file the agent receives
and handed them to a section the hard ceiling then threw away — the guard's
own failure mode, one layer down. tokio lost 1,298 the same way.

1. `owedPayableBelow` — hold back only the prefix of what is owed below that
   the response can still PAY, in rank order. A promise the ceiling cannot
   reach is not a claim on this file's bytes.

2. The final truncation now spends the EPILOGUE before it spends a rendered
   file section. It used to cut at the last section header, dropping that
   section AND the trailing notes; dropping the notes alone is almost always
   enough. A section is source the agent otherwise has to Read; the epilogue
   is a pointer list and two reminders, and the note that replaces it carries
   the "explore these names" instruction forward.

Also count `flow.text` in `totalChars`. It is prepended to `lines` to make the
final output, so the render loop always spent against a ceiling it was ~2K
under on symbol-bag queries.

Deterministic, same clean-rebuilt indexes, both builds (baseline = CG-30 tip):

  repo         base source   new source      files
  django            20,033       20,791   5 trunc -> 6
  excalidraw        18,776       20,204   7 trunc -> 8
  okhttp            15,628       19,034   4 trunc -> 5
  tokio             20,340       21,521   4 trunc -> 5
  gin               10,776       10,776   4 -> 4   (byte-identical)
  alamofire         11,662       11,662   2 -> 2   (byte-identical)

No repo delivers less; four stop truncating. `funded` in the diagnostic now
reports the render CEILING the guard allows, which is what every render path
is actually bounded by.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Colby McHenry
2026-08-06 02:59:46 -05:00
co-authored by Claude Opus 5
parent 089dcc276f
commit f1fecb8232
3 changed files with 110 additions and 43 deletions
+13 -2
View File
@@ -151,9 +151,11 @@ describe('CG-31 — the cluster path holds back what is still owed below it', ()
// shape that makes the bounded overshoot fire at all.
const source = fs.readFileSync(path.join(testDir, GIANT), 'utf-8');
expect(source.length).toBeGreaterThan((rec.spendable ?? 0) * 2);
// And the guard actually bit — a vacuous pass here would hide a regression.
// And the guard actually bit — a vacuous pass here would hide a
// regression. Measured against the bounded overshoot a cluster's top
// member may otherwise take (1.5x, CG-30), which is what it refused.
expect(rec.funded).not.toBeNull();
expect(rec.funded!).toBeLessThan(rec.spendable!);
expect(rec.funded!).toBeLessThan(Math.round(rec.spendable! * 1.5));
});
});
@@ -203,6 +205,15 @@ describe('CG-31 — the cluster path holds back what is still owed below it', ()
}
});
it('nothing is lost to the hard ceiling — the epilogue is cut before a section', () => {
// A section thrown away by the final truncation is the same starvation
// arriving after the guard has done its work: the bytes were held back
// for that file and then nobody received them.
for (const probe of [spread, precise]) {
expect(probe.report.files.filter((f) => f.render === 'dropped')).toEqual([]);
}
});
it('keeps the response inside the hard ceiling', () => {
for (const probe of [spread, precise]) {
expect(probe.report.envelope.chars).toBeLessThanOrEqual(probe.report.budget.hardCeiling);