fix(compact): count the logged session prefix toward token pressure

ds-review-bot critical: compactIfNeeded estimated pressure from the
derived history + system prompt only, but every loop-built request also
carries EpochHeader.messagePrefix in front of the history — a
deployment at the window edge would under-estimate by exactly the
prefix, skip compaction, and ship an over-window request.

BasicCompactService now gates on estimatePressure(): the session prefix
read from the log's folded header + the derived history + the system
prompt. The fold is exact from the instance's second request on (and
from a resumed instance's first — the previous instance logged its
prefix); it is absent only before a fresh session's first request,
where the history is a single prompt and compaction is moot. Compaction
itself still shrinks history only — a prefix that alone approaches the
window is a configuration error no compactor fixes, same as the
documented single-unit-overflow stance.
This commit is contained in:
Yichen Jiang
2026-07-08 19:32:42 +08:00
parent 77333c0a19
commit e6bd6cc9be
3 changed files with 55 additions and 7 deletions
@@ -557,6 +557,29 @@ describe('BasicCompactService.compactIfNeeded', () => {
expect(result!.shadowedSeqs.length).toBeGreaterThan(0)
})
it('counts the logged session prefix toward pressure (every request carries it in front of the history)', async () => {
const svc = createTestService({ contextWindow: 200, thresholdRatio: 0.5, retainTokens: 10 })
const session = multiTurnSession(3, 1) // 6 derived messages ≈ 84 estimated tokens — under the 100 threshold alone
expect(await compactIfNeeded(svc, session, '', 'm', SIGNAL)).toBeNull()
// The loop records the composed agent/session-prefix product on the
// request header; it rides every request, so pressure must include it.
session.append('request/header', {
header: {
config: { model: 'm' },
messagePrefix: [
{ role: 'user', content: [{ type: 'text', text: `opener one.${LONG_FIXTURE_TEXT}` }] },
{ role: 'user', content: [{ type: 'text', text: `opener two.${LONG_FIXTURE_TEXT}` }] },
],
},
reason: 'initial',
})
const result = await compactIfNeeded(svc, session, '', 'm', SIGNAL)
expect(result).not.toBeNull()
// The prefix itself is NOT history: compaction shadowed surface nodes only.
expect(session.requestHeader()?.messagePrefix).toHaveLength(2)
})
it('returns the first compaction result when a zero-retry pass converges after the loop', async () => {
// With compactionRetries=0 there is no next-loop threshold check after the
// first mutation, so the success path is the post-loop `return result`.