feat(core): elide historic middle-pane note snapshots from model context

Every user message sent while a note is open carries a full snapshot of
that note in userMessageContext, so a long chat over one open note
resends N full copies on every model call. The elision decorator now
rewrites prior-turn user messages to keep the pane kind and path but
replace note content above a small floor with a placeholder pointing at
the still-readable file. The current message's snapshot is untouched,
so "summarize this" keeps working; the system prompt already tells the
model later middle-pane context overrides earlier. Config:
elideHistoricMiddlePaneContent, default on.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Arjun 2026-07-07 03:33:22 +05:30 committed by Ramnique Singh
parent 05d7fa41cf
commit 4fbca2dbad
3 changed files with 124 additions and 5 deletions

View file

@ -393,18 +393,21 @@ Rules:
The app wires the resolver through a decorator (`context-elision.ts`) that
applies transmit-time elision to the materialized prefix: tool results from
prior turns above a size threshold are replaced with a short placeholder
telling the model to re-run the tool if it needs the output, and inline
image parts from prior turns (video-mode webcam and screen-share frames) are
telling the model to re-run the tool if it needs the output; inline image
parts from prior turns (video-mode webcam and screen-share frames) are
replaced with a text part recording how many frames of each kind were
dropped. The durable log is untouched; only the transmitted bytes change.
dropped; and middle-pane note snapshots on prior-turn user messages keep
their kind and path but have their content replaced with a placeholder
pointing at the still-readable file. The durable log is untouched; only the
transmitted bytes change.
Elision is a pure function of each message, so resolved prefixes stay
byte-stable across calls (provider prefix caches keep working), and the
current turn's own messages never pass through the resolver, so in-flight
tool results and just-captured frames are always sent verbatim. Policy lives
in `config/context.json` (`elideHistoricToolResults`, default true;
`elideHistoricToolResultsThresholdChars`, default 10000;
`elideHistoricImages`, default true). The inspect CLI composes through the
same decorated resolver.
`elideHistoricImages`, default true; `elideHistoricMiddlePaneContent`,
default true). The inspect CLI composes through the same decorated resolver.
### 6.7 Agent snapshot inheritance

View file

@ -4,6 +4,7 @@ import type { TurnContext, TurnEvent } from "@x/shared/dist/turns.js";
import {
ElidingContextResolver,
elideHistoricImages,
elideHistoricMiddlePaneContent,
elideHistoricToolResults,
} from "./context-elision.js";
import { TurnRepoContextResolver } from "./context-resolver.js";
@ -238,10 +239,60 @@ describe("elideHistoricImages", () => {
});
});
function noteMessage(content: string) {
return {
role: "user" as const,
content: "make this punchier",
userMessageContext: {
currentDateTime: "Thursday, July 2, 2026 at 10:00 AM GMT",
middlePane: {
kind: "note" as const,
path: "knowledge/Notes/Draft.md",
content,
},
},
};
}
describe("elideHistoricMiddlePaneContent", () => {
it("replaces large note snapshots, keeping kind and path", () => {
const [elided] = elideHistoricMiddlePaneContent([
noteMessage("n".repeat(600)),
]);
if (elided.role !== "user") throw new Error("expected user message");
const middlePane = elided.userMessageContext?.middlePane;
if (middlePane?.kind !== "note") throw new Error("expected note pane");
expect(middlePane.path).toBe("knowledge/Notes/Draft.md");
expect(middlePane.content).toContain("omitted from history");
expect(middlePane.content).toContain("600");
expect(elided.userMessageContext?.currentDateTime).toBeDefined();
expect(elided.content).toBe("make this punchier");
});
it("keeps small notes and non-note panes untouched", () => {
const small = noteMessage("short todo list");
const browser = {
role: "user" as const,
content: "what is this page?",
userMessageContext: {
middlePane: {
kind: "browser" as const,
url: "https://example.com",
title: "Example",
},
},
};
const plain = user("no context at all");
const messages = [small, browser, plain];
expect(elideHistoricMiddlePaneContent(messages)).toEqual(messages);
});
});
const POLICY_OFF = {
toolResults: false,
toolResultThresholdChars: 100,
images: false,
middlePaneContent: false,
};
describe("ElidingContextResolver", () => {
@ -327,4 +378,23 @@ describe("ElidingContextResolver", () => {
expect(input.content.some((p) => p.type === "image")).toBe(false);
expect(JSON.stringify(input.content)).toContain("1 webcam frame");
});
it("elides middle-pane note snapshots in the resolved prefix when enabled", async () => {
const repo = new InMemoryTurnRepo();
const log = toolTurnLog(T1, [], "small").map((event) =>
event.type === "turn_created"
? { ...event, input: noteMessage("n".repeat(600)) }
: event,
);
repo.seed(log);
const resolved = await resolver(repo, {
...POLICY_OFF,
middlePaneContent: true,
}).resolve({ previousTurnId: T1 });
const input = resolved[0];
if (input.role !== "user") throw new Error("expected user message");
const middlePane = input.userMessageContext?.middlePane;
if (middlePane?.kind !== "note") throw new Error("expected note pane");
expect(middlePane.content).toContain("omitted from history");
});
});

View file

@ -25,6 +25,11 @@ import type { ITurnRepo } from "./repo.js";
// text carries the takeaway, and fresh frames arrive with every new call
// message. Unlike tool results they cannot be re-fetched, so the
// placeholder only records what was there.
// - Middle-pane note content: every user message sent while a note is
// open carries a full snapshot of that note in userMessageContext. Only
// the newest snapshot matters (the system prompt already tells the model
// later middle-pane context overrides earlier), and the current file is
// always re-readable at the recorded path.
//
// Elision is a pure function of each message's content, so resolved prefixes
// stay byte-stable across calls and turns (provider prefix caches keep
@ -35,18 +40,25 @@ export interface ElisionPolicy {
toolResults: boolean;
toolResultThresholdChars: number;
images: boolean;
middlePaneContent: boolean;
}
export const DEFAULT_ELISION_POLICY: ElisionPolicy = {
toolResults: true,
toolResultThresholdChars: 10_000,
images: true,
middlePaneContent: true,
};
// Notes smaller than this stay verbatim in history: below the floor the
// placeholder saves nothing and a short note may carry useful context.
const MIDDLE_PANE_CONTENT_FLOOR_CHARS = 500;
const ContextConfig = z.object({
elideHistoricToolResults: z.boolean().optional(),
elideHistoricToolResultsThresholdChars: z.number().int().min(0).optional(),
elideHistoricImages: z.boolean().optional(),
elideHistoricMiddlePaneContent: z.boolean().optional(),
});
const CONTEXT_CONFIG_PATH = path.join(WorkDir, "config", "context.json");
@ -70,6 +82,9 @@ export function loadElisionPolicy(): ElisionPolicy {
DEFAULT_ELISION_POLICY.toolResultThresholdChars,
images:
parsed.elideHistoricImages ?? DEFAULT_ELISION_POLICY.images,
middlePaneContent:
parsed.elideHistoricMiddlePaneContent ??
DEFAULT_ELISION_POLICY.middlePaneContent,
};
} catch {
return DEFAULT_ELISION_POLICY;
@ -124,6 +139,34 @@ export function elideHistoricImages(
});
}
export function elideHistoricMiddlePaneContent(
messages: Array<z.infer<typeof ConversationMessage>>,
): Array<z.infer<typeof ConversationMessage>> {
return messages.map((message) => {
if (message.role !== "user") {
return message;
}
const middlePane = message.userMessageContext?.middlePane;
if (
!middlePane ||
middlePane.kind !== "note" ||
middlePane.content.length <= MIDDLE_PANE_CONTENT_FLOOR_CHARS
) {
return message;
}
return {
...message,
userMessageContext: {
...message.userMessageContext,
middlePane: {
...middlePane,
content: `[Note content (${middlePane.content.length} characters) omitted from history to save context. This snapshot is stale; read the file at the path above if its content is needed now.]`,
},
},
};
});
}
// IContextResolver decorator: applies the elision policy to the materialized
// cross-turn prefix. Agent snapshot resolution is delegated untouched.
export class ElidingContextResolver implements IContextResolver {
@ -155,6 +198,9 @@ export class ElidingContextResolver implements IContextResolver {
if (policy.images) {
prefix = elideHistoricImages(prefix);
}
if (policy.middlePaneContent) {
prefix = elideHistoricMiddlePaneContent(prefix);
}
return prefix;
}