feat(coding-agent): added tracking for expected cache invalidations
- Added tracking for expected cache invalidations during model changes, compactions, and plan-mode transitions. - Included `cacheMissExplainedAt` metadata in session context to prevent displaying misleading cache miss warnings in the transcript. - Updated controller logic to reset assistant usage markers when mode-switching or performing actions that invalidate the prompt cache.
This commit is contained in:
@@ -980,12 +980,14 @@ export class EventController {
|
||||
}
|
||||
this.ctx.showWarning(event.errorMessage);
|
||||
} else if (!event.skipped) {
|
||||
this.ctx.lastAssistantUsage = undefined;
|
||||
this.ctx.rebuildChatFromMessages();
|
||||
this.ctx.statusLine.invalidate();
|
||||
this.ctx.updateEditorTopBorder();
|
||||
this.ctx.showStatus("Auto-shake completed");
|
||||
}
|
||||
} else if (event.result) {
|
||||
this.ctx.lastAssistantUsage = undefined;
|
||||
this.ctx.rebuildChatFromMessages();
|
||||
this.ctx.statusLine.invalidate();
|
||||
this.ctx.updateEditorTopBorder();
|
||||
@@ -993,6 +995,7 @@ export class EventController {
|
||||
this.ctx.showWarning(event.errorMessage);
|
||||
} else if (isHandoffAction) {
|
||||
this.ctx.chatContainer.clear();
|
||||
this.ctx.lastAssistantUsage = undefined;
|
||||
this.ctx.rebuildChatFromMessages();
|
||||
this.ctx.statusLine.invalidate();
|
||||
this.ctx.updateEditorTopBorder();
|
||||
|
||||
@@ -1860,6 +1860,9 @@ export class InteractiveMode implements InteractiveModeContext {
|
||||
this.#planModePreviousTools = previousTools;
|
||||
this.planModePlanFilePath = planFilePath;
|
||||
this.planModeEnabled = true;
|
||||
// Suppress cache-miss marker on the next turn: plan mode changes the system
|
||||
// prompt, which predictably invalidates the cache.
|
||||
this.lastAssistantUsage = undefined;
|
||||
|
||||
await this.session.setActiveToolsByName(uniquePlanTools);
|
||||
this.session.setPlanModeState({
|
||||
@@ -1977,6 +1980,9 @@ export class InteractiveMode implements InteractiveModeContext {
|
||||
this.session.setStandingResolveHandler?.(null);
|
||||
this.session.setPlanModeState(undefined);
|
||||
this.planModeEnabled = false;
|
||||
// Suppress cache-miss marker on the next turn: plan exit changes the system
|
||||
// prompt, which predictably invalidates the cache.
|
||||
this.lastAssistantUsage = undefined;
|
||||
this.planModePaused = options?.paused ?? false;
|
||||
this.planModePlanFilePath = undefined;
|
||||
this.#planModePreviousTools = undefined;
|
||||
|
||||
@@ -403,7 +403,10 @@ export class UiHelpers {
|
||||
// updateResult armed.
|
||||
previous.seal();
|
||||
};
|
||||
for (const message of sessionContext.messages) {
|
||||
const messages = sessionContext.messages;
|
||||
const count = messages.length;
|
||||
for (let i = 0; i < count; i++) {
|
||||
const message = messages[i]!;
|
||||
if (message.role !== "toolResult") flushPendingUsage();
|
||||
// Assistant messages need special handling for tool calls
|
||||
if (message.role === "assistant") {
|
||||
@@ -412,7 +415,8 @@ export class UiHelpers {
|
||||
const assistantComponent = lastChild instanceof AssistantMessageComponent ? lastChild : undefined;
|
||||
if (assistantComponent) {
|
||||
const usage = message.usage;
|
||||
if (this.ctx.settings.get("display.cacheMissMarker")) {
|
||||
const explained = sessionContext.cacheMissExplainedAt?.[i] ?? false;
|
||||
if (this.ctx.settings.get("display.cacheMissMarker") && !explained) {
|
||||
const invalidation = detectCacheInvalidation(this.ctx.lastAssistantUsage, usage);
|
||||
if (invalidation) assistantComponent.setCacheInvalidation(invalidation);
|
||||
}
|
||||
|
||||
@@ -20,6 +20,13 @@ export interface SessionContext {
|
||||
mode: string;
|
||||
/** Mode-specific data from the last mode_change entry */
|
||||
modeData?: Record<string, unknown>;
|
||||
/**
|
||||
* Array parallel to messages, indicating which assistant turns should
|
||||
* have their prompt-cache misses suppressed/explained (because a model,
|
||||
* compaction, or plan-mode transition directly preceded them).
|
||||
* Only populated in transcript mode.
|
||||
*/
|
||||
cacheMissExplainedAt?: boolean[];
|
||||
}
|
||||
|
||||
/** Lists session model strings to try when restoring, in fallback order. */
|
||||
@@ -191,12 +198,45 @@ export function buildSessionContext(
|
||||
// 2. Emit kept messages (from firstKeptEntryId up to compaction)
|
||||
// 3. Emit messages after compaction
|
||||
const messages: AgentMessage[] = [];
|
||||
const cacheMissExplainedAt: boolean[] = [];
|
||||
let pendingReset = false;
|
||||
let currentMode = "none";
|
||||
let lastAssistantModel: string | undefined;
|
||||
|
||||
const handleEntryResetTracking = (entry: SessionEntry) => {
|
||||
if (entry.type === "compaction") {
|
||||
pendingReset = true;
|
||||
} else if (entry.type === "model_change") {
|
||||
pendingReset = true;
|
||||
} else if (entry.type === "mode_change") {
|
||||
const isPlanTransition = (entry.mode === "plan") !== (currentMode === "plan");
|
||||
if (isPlanTransition) {
|
||||
pendingReset = true;
|
||||
}
|
||||
currentMode = entry.mode;
|
||||
}
|
||||
};
|
||||
|
||||
const pushMessage = (msg: AgentMessage) => {
|
||||
messages.push(msg);
|
||||
if (!options?.transcript) return;
|
||||
if (msg.role === "assistant") {
|
||||
const currentModel = `${msg.provider}/${msg.model}`;
|
||||
const modelChanged = lastAssistantModel !== undefined && lastAssistantModel !== currentModel;
|
||||
lastAssistantModel = currentModel;
|
||||
cacheMissExplainedAt.push(pendingReset || modelChanged);
|
||||
pendingReset = false;
|
||||
} else {
|
||||
cacheMissExplainedAt.push(false);
|
||||
}
|
||||
};
|
||||
|
||||
const appendMessage = (entry: SessionEntry) => {
|
||||
handleEntryResetTracking(entry);
|
||||
if (entry.type === "message") {
|
||||
messages.push(entry.message);
|
||||
pushMessage(entry.message);
|
||||
} else if (entry.type === "custom_message") {
|
||||
messages.push(
|
||||
pushMessage(
|
||||
createCustomMessage(
|
||||
entry.customType,
|
||||
entry.content,
|
||||
@@ -207,7 +247,7 @@ export function buildSessionContext(
|
||||
),
|
||||
);
|
||||
} else if (entry.type === "branch_summary" && entry.summary) {
|
||||
messages.push(createBranchSummaryMessage(entry.summary, entry.fromId, entry.timestamp));
|
||||
pushMessage(createBranchSummaryMessage(entry.summary, entry.fromId, entry.timestamp));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -217,9 +257,10 @@ export function buildSessionContext(
|
||||
// TUI) at the point it fired, with any snapcompact frames re-attached so
|
||||
// the component can report them.
|
||||
for (const entry of path) {
|
||||
handleEntryResetTracking(entry);
|
||||
if (entry.type === "compaction") {
|
||||
const snapcompactArchive = snapcompact.getPreservedArchive(entry.preserveData);
|
||||
messages.push(
|
||||
pushMessage(
|
||||
createCompactionSummaryMessage(
|
||||
entry.summary,
|
||||
entry.tokensBefore,
|
||||
@@ -252,7 +293,7 @@ export function buildSessionContext(
|
||||
// Emit summary first; re-attach any archived snapcompact frames so the
|
||||
// model can keep reading the archived history after every context rebuild.
|
||||
const snapcompactArchive = snapcompact.getPreservedArchive(compaction.preserveData);
|
||||
messages.push(
|
||||
pushMessage(
|
||||
createCompactionSummaryMessage(
|
||||
compaction.summary,
|
||||
compaction.tokensBefore,
|
||||
@@ -335,6 +376,9 @@ export function buildSessionContext(
|
||||
);
|
||||
if (normalized.length === 0) {
|
||||
messages.splice(i, 1);
|
||||
if (options?.transcript) {
|
||||
cacheMissExplainedAt.splice(i, 1);
|
||||
}
|
||||
} else {
|
||||
messages[i] = { ...message, content: normalized };
|
||||
}
|
||||
@@ -342,6 +386,7 @@ export function buildSessionContext(
|
||||
|
||||
return {
|
||||
messages,
|
||||
cacheMissExplainedAt: options?.transcript ? cacheMissExplainedAt : undefined,
|
||||
thinkingLevel,
|
||||
serviceTier,
|
||||
models,
|
||||
|
||||
@@ -111,7 +111,6 @@ describe("formatSessionDumpText tool parameters", () => {
|
||||
expect(out).not.toContain("## Available Tools");
|
||||
});
|
||||
|
||||
|
||||
it("does not falsely omit the Available Tools section even if systemPrompt contains tool headings", () => {
|
||||
const out = formatSessionDumpText({
|
||||
messages: [],
|
||||
|
||||
Reference in New Issue
Block a user