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:
@@ -187,7 +187,7 @@ export class BasicCompactService extends CompactService {
|
||||
try {
|
||||
const result = await this.compactIfNeeded(agent, fullSystemPrompt, signal)
|
||||
if (result) {
|
||||
const after = this.estimateTokens(agent.session.deriveMessages(), fullSystemPrompt)
|
||||
const after = this.estimatePressure(agent.session, fullSystemPrompt)
|
||||
ctx.logger.info(
|
||||
`compaction: shadowed ${result.shadowedSeqs.length} surface nodes ` +
|
||||
`(seqs ${result.shadowedRange.start}-${result.shadowedRange.end}, ` +
|
||||
@@ -359,11 +359,19 @@ export class BasicCompactService extends CompactService {
|
||||
// ---- Core API (implements the abstract contract) ----
|
||||
|
||||
/**
|
||||
* The sole token-pressure gate: estimate the current surface-derived history,
|
||||
* and if it exceeds the threshold (`contextWindow * thresholdRatio`), compact
|
||||
* The sole token-pressure gate: estimate the NEXT request's pressure — the
|
||||
* logged session prefix + the surface-derived history + the system prompt
|
||||
* ({@link estimatePressure}) — and if it exceeds the threshold
|
||||
* (`contextWindow * thresholdRatio`), compact
|
||||
* the oldest surface nodes outside the `retainTokens` budget. The auto-
|
||||
* compaction listener delegates here rather than pre-checking, so this is the
|
||||
* only place the decision lives.
|
||||
* only place the decision lives. The prefix counts because every request
|
||||
* carries it in front of the history (`EpochHeader.messagePrefix`) even
|
||||
* though it is not derived history — omitting it would under-estimate by
|
||||
* exactly the prefix and let a deployment at the window edge skip
|
||||
* compaction, then ship an over-window request. Compaction itself can only
|
||||
* shrink HISTORY: a prefix that alone approaches the window is a
|
||||
* configuration error no compactor fixes.
|
||||
*
|
||||
* Retention is a UNIFORM tail→head walk over the whole surface — turn
|
||||
* boundaries play NO role. Walking node-by-node from the tail and summing
|
||||
@@ -393,7 +401,7 @@ export class BasicCompactService extends CompactService {
|
||||
const threshold = Math.floor(this.config.contextWindow * this.config.thresholdRatio)
|
||||
let result: CompactionResult | null = null
|
||||
for (let attempt = 0; attempt <= this.config.compactionRetries; attempt++) {
|
||||
const totalTokens = this.estimateTokens(session.deriveMessages(), fullSystemPrompt)
|
||||
const totalTokens = this.estimatePressure(session, fullSystemPrompt)
|
||||
if (totalTokens < threshold) return result
|
||||
|
||||
const range = this._compactableRange(session)
|
||||
@@ -407,7 +415,7 @@ export class BasicCompactService extends CompactService {
|
||||
result = await this.compactRegion(session, range.start, range.end, agent, signal)
|
||||
}
|
||||
|
||||
const totalTokens = this.estimateTokens(session.deriveMessages(), fullSystemPrompt)
|
||||
const totalTokens = this.estimatePressure(session, fullSystemPrompt)
|
||||
if (totalTokens < threshold) return result
|
||||
|
||||
throw new Error(
|
||||
@@ -416,6 +424,23 @@ export class BasicCompactService extends CompactService {
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Estimated token pressure of the NEXT request: the logged session prefix
|
||||
* (`EpochHeader.messagePrefix` from the header fold — request-only messages
|
||||
* the loop sends in front of the derived history), the derived history, and
|
||||
* the system prompt. The fold is exact from the loop 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.
|
||||
* @param session - the session whose next request is being estimated.
|
||||
* @param fullSystemPrompt - the assembled system prompt (counts toward pressure).
|
||||
* @returns the estimated token total the next request will carry.
|
||||
*/
|
||||
estimatePressure(session: Session, fullSystemPrompt: string): number {
|
||||
const sessionPrefix = session.requestHeader()?.messagePrefix ?? []
|
||||
return this.estimateTokens([...sessionPrefix, ...session.deriveMessages()], fullSystemPrompt)
|
||||
}
|
||||
|
||||
override async compactRegion(
|
||||
session: Session,
|
||||
start: number,
|
||||
|
||||
Reference in New Issue
Block a user