fix(coding-agent): show unknown context usage after compaction, fix multi-compaction boundary

After compaction, context token count is unknown until the next LLM
response. Instead of showing stale pre-compaction values or heuristic
estimates, the footer now shows ?/200k.

ContextUsage.tokens and ContextUsage.percent are now number | null
(breaking change). Removed usageTokens, trailingTokens, lastUsageIndex
from ContextUsage (internal details).

Also fixed _checkCompaction() using .find() (first compaction) instead
of getLatestCompactionEntry() (latest), which caused incorrect overflow
detection with multiple compactions.

Closes #1382
This commit is contained in:
Mario Zechner 2026-02-12 18:35:09 +01:00
parent 1e88c5e463
commit 7eb969ddb1
5 changed files with 56 additions and 29 deletions

View file

@ -68,6 +68,7 @@ import type { ModelRegistry } from "./model-registry.js";
import { expandPromptTemplate, type PromptTemplate } from "./prompt-templates.js";
import type { ResourceExtensionPaths, ResourceLoader } from "./resource-loader.js";
import type { BranchSummaryEntry, CompactionEntry, SessionManager } from "./session-manager.js";
import { getLatestCompactionEntry } from "./session-manager.js";
import type { SettingsManager } from "./settings-manager.js";
import { BUILTIN_SLASH_COMMANDS, type SlashCommandInfo, type SlashCommandLocation } from "./slash-commands.js";
import { buildSystemPrompt } from "./system-prompt.js";
@ -1531,9 +1532,9 @@ export class AgentSession {
// The error shouldn't trigger another compaction since we already compacted.
// Example: opus fails → switch to codex → compact → switch back to opus → opus error
// is still in context but shouldn't trigger compaction again.
const compactionEntry = this.sessionManager.getBranch().find((e) => e.type === "compaction");
const compactionEntry = getLatestCompactionEntry(this.sessionManager.getBranch());
const errorIsFromBeforeCompaction =
compactionEntry && assistantMessage.timestamp < new Date(compactionEntry.timestamp).getTime();
compactionEntry !== null && assistantMessage.timestamp < new Date(compactionEntry.timestamp).getTime();
// Case 1: Overflow - LLM returned context overflow error
if (sameModel && !errorIsFromBeforeCompaction && isContextOverflow(assistantMessage, contextWindow)) {
@ -2697,6 +2698,35 @@ export class AgentSession {
const contextWindow = model.contextWindow ?? 0;
if (contextWindow <= 0) return undefined;
// After compaction, the last assistant usage reflects pre-compaction context size.
// We can only trust usage from an assistant that responded after the latest compaction.
// If no such assistant exists, context token count is unknown until the next LLM response.
const branchEntries = this.sessionManager.getBranch();
const latestCompaction = getLatestCompactionEntry(branchEntries);
if (latestCompaction) {
// Check if there's a valid assistant usage after the compaction boundary
const compactionIndex = branchEntries.lastIndexOf(latestCompaction);
let hasPostCompactionUsage = false;
for (let i = branchEntries.length - 1; i > compactionIndex; i--) {
const entry = branchEntries[i];
if (entry.type === "message" && entry.message.role === "assistant") {
const assistant = entry.message;
if (assistant.stopReason !== "aborted" && assistant.stopReason !== "error") {
const contextTokens = calculateContextTokens(assistant.usage);
if (contextTokens > 0) {
hasPostCompactionUsage = true;
}
break;
}
}
}
if (!hasPostCompactionUsage) {
return { tokens: null, contextWindow, percent: null };
}
}
const estimate = estimateContextTokens(this.messages);
const percent = (estimate.tokens / contextWindow) * 100;
@ -2704,9 +2734,6 @@ export class AgentSession {
tokens: estimate.tokens,
contextWindow,
percent,
usageTokens: estimate.usageTokens,
trailingTokens: estimate.trailingTokens,
lastUsageIndex: estimate.lastUsageIndex,
};
}

View file

@ -235,12 +235,11 @@ export interface ExtensionUIContext {
// ============================================================================
export interface ContextUsage {
tokens: number;
/** Estimated context tokens, or null if unknown (e.g. right after compaction, before next LLM response). */
tokens: number | null;
contextWindow: number;
percent: number;
usageTokens: number;
trailingTokens: number;
lastUsageIndex: number | null;
/** Context usage as percentage of context window, or null if tokens is unknown. */
percent: number | null;
}
export interface CompactOptions {