feat: unify context compaction

This commit is contained in:
云服务部-叶林立
2026-09-01 14:52:43 +08:00
parent 44f27f7585
commit a4807dde67
114 changed files with 2681 additions and 21336 deletions
+35
View File
@@ -0,0 +1,35 @@
import test from "node:test";
import assert from "node:assert/strict";
import { DEFAULT_CONFIG, normalizeConfig } from "../src/config.ts";
test("normalizes one greenfield piCompaction config without legacy aliases", () => {
const config = normalizeConfig({
enabled: false,
softRatio: 0.7,
hardRatio: 0.91,
targetRatio: 0.5,
deterministic: { enabled: false, maxStubTokens: 512 },
semantic: { enabled: false, concurrency: 2, maxRetries: 0 },
native: { enabled: false },
});
assert.equal(config.enabled, false);
assert.equal(config.deterministic.enabled, false);
assert.equal(config.deterministic.maxStubTokens, 512);
assert.equal(config.semantic.enabled, false);
assert.equal(config.semantic.maxRetries, 0);
assert.equal(config.native.enabled, false);
});
test("invalid ratios and bounds fail safe to bounded defaults", () => {
const config = normalizeConfig({
softRatio: 2,
hardRatio: -1,
targetRatio: 0.99,
semantic: { concurrency: 50, maxRetries: -1 },
});
assert.equal(config.softRatio, DEFAULT_CONFIG.softRatio);
assert.ok(config.targetRatio < config.softRatio);
assert.ok(config.hardRatio > config.softRatio);
assert.equal(config.semantic.concurrency, DEFAULT_CONFIG.semantic.concurrency);
assert.equal(config.semantic.maxRetries, DEFAULT_CONFIG.semantic.maxRetries);
});
@@ -0,0 +1,103 @@
import test from "node:test";
import assert from "node:assert/strict";
import { performance } from "node:perf_hooks";
import piCompaction from "../index.ts";
function harness() {
const handlers = new Map<string, Function[]>();
const tools = new Map<string, any>();
const commands = new Map<string, any>();
const entries: Array<{ customType: string; data: unknown }> = [];
const toolDefinitions = [{ name: "bash", description: "shell", parameters: { type: "object" } }];
const pi: any = {
on(name: string, handler: Function) {
const list = handlers.get(name) ?? [];
list.push(handler);
handlers.set(name, list);
},
appendEntry(customType: string, data: unknown) { entries.push({ customType, data }); },
registerTool(tool: any) { tools.set(tool.name, tool); },
registerCommand(name: string, command: any) { commands.set(name, command); },
getActiveTools() { return ["bash"]; },
getAllTools() { return toolDefinitions; },
};
return { pi, handlers, tools, commands, entries };
}
function context(branch: any[] = []) {
return {
sessionManager: {
getSessionId: () => "session-test",
getBranch: () => branch,
},
model: { provider: "test", api: "test", id: "model", contextWindow: 100_000 },
modelRegistry: {},
getSystemPrompt: () => "system",
getContextUsage: () => ({ tokens: 1_000, contextWindow: 100_000, percent: 1 }),
hasUI: false,
abort() {},
} as any;
}
test("one runtime projects tool evidence and recovers it through context_tree_query", async () => {
const h = harness();
piCompaction(h.pi);
const ctx = context();
await h.handlers.get("session_start")![0]!({}, ctx);
const assistant = {
role: "assistant",
content: [{ type: "toolCall", id: "call-1", name: "bash", arguments: { command: "npm test" } }],
timestamp: 10,
};
const result = {
role: "toolResult",
toolCallId: "call-1",
toolName: "bash",
content: [{ type: "text", text: "failure evidence" }],
isError: true,
timestamp: 11,
};
await h.handlers.get("turn_end")![0]!({ message: assistant, toolResults: [result] }, ctx);
const projected = await h.handlers.get("context")![0]!({ messages: [assistant, result] }, ctx);
assert.match(projected.messages[1].content[0].text, /pi-compaction tool=bash/);
const cacheStart = performance.now();
for (let iteration = 0; iteration < 1_000; iteration++) {
await h.handlers.get("context")![0]!({ messages: [assistant, result] }, ctx);
}
const cacheAverageMs = (performance.now() - cacheStart) / 1_000;
assert.ok(cacheAverageMs < 2, `full context handler cache-hit average ${cacheAverageMs.toFixed(3)}ms exceeded 2ms`);
assert.equal(h.entries.length, 1);
assert.equal(h.entries[0]!.customType, "pi-compaction");
const query = h.tools.get("context_tree_query");
const recovered = await query.execute("q", { toolCallIds: ["call-1"] }, new AbortController().signal, undefined, ctx);
assert.match(recovered.content[0].text, /failure evidence/);
await h.handlers.get("session_shutdown")![0]!({}, ctx);
});
test("model switching after a native checkpoint projects the portable checkpoint", async () => {
const h = harness();
piCompaction(h.pi);
const details = {
kind: "pi-compaction",
version: 1,
checkpoint: "native",
modelKey: "openai-codex/openai-codex-responses/gpt-old",
replacementHistory: [{ type: "compaction", encrypted_content: "opaque" }],
portableSummary: "decisions and files from the old model",
sourceFingerprint: "old",
};
const branch = [
{ type: "compaction", id: "c1", parentId: null, timestamp: new Date().toISOString(), summary: "marker", firstKeptEntryId: "m0", tokensBefore: 1_000, details },
{ type: "message", id: "m1", parentId: "c1", timestamp: new Date().toISOString(), message: { role: "user", content: "continue", timestamp: 20 } },
] as any[];
const ctx = context(branch);
await h.handlers.get("session_start")![0]!({}, ctx);
const projected = await h.handlers.get("context")![0]!({ messages: [] }, ctx);
assert.match(projected.messages[0].content[0].text, /decisions and files/);
assert.equal(projected.messages[1].content, "continue");
await h.handlers.get("session_shutdown")![0]!({}, ctx);
});
@@ -0,0 +1,50 @@
import test from "node:test";
import assert from "node:assert/strict";
import {
buildReplacementHistory,
findNativeCheckpoint,
parseNativeCompactionDetails,
type NativeCompactionDetails,
} from "../src/native-compaction.ts";
const details: NativeCompactionDetails = {
kind: "pi-compaction",
version: 1,
checkpoint: "native",
modelKey: "openai-codex/openai-codex-responses/gpt-test",
replacementHistory: [{ type: "compaction", encrypted_content: "opaque" }],
portableSummary: "portable",
sourceFingerprint: "abc",
};
test("recognizes only the unified native checkpoint format", () => {
assert.deepEqual(parseNativeCompactionDetails(details), details);
assert.equal(parseNativeCompactionDetails({ ...details, kind: "openai-codex-native-compaction" }), undefined);
assert.equal(parseNativeCompactionDetails({ ...details, portableSummary: undefined }), undefined);
});
test("latest Pi compaction entry is the authoritative native checkpoint", () => {
const branch = [
{ type: "message", id: "m1" },
{ type: "compaction", id: "c1", details },
] as any;
const found = findNativeCheckpoint(branch);
assert.equal(found.status, "valid");
if (found.status === "valid") assert.equal(found.checkpoint.entryIndex, 1);
const shadowed = findNativeCheckpoint([...branch, { type: "compaction", id: "c2", details: { kind: "other" } }] as any);
assert.equal(shadowed.status, "none");
const portable = findNativeCheckpoint([
...branch,
{ type: "compaction", id: "c3", details: { kind: "pi-compaction", version: 1, checkpoint: "portable" } },
] as any);
assert.equal(portable.status, "none");
});
test("replacement history ends with exactly one native item", () => {
const history = buildReplacementHistory([
{ role: "user", content: [{ type: "input_text", text: "recent" }] },
], { type: "compaction", encrypted_content: "opaque" }, 1_000);
assert.equal(history.at(-1)?.type, "compaction");
assert.equal(history.filter((item) => item.type === "compaction").length, 1);
});
+29
View File
@@ -0,0 +1,29 @@
import test from "node:test";
import assert from "node:assert/strict";
import { performance } from "node:perf_hooks";
import { DEFAULT_CONFIG } from "../src/config.ts";
import { SegmentIndex } from "../src/segment-index.ts";
function message(index: number) {
return { role: "user", content: `message-${index}`, timestamp: index + 1 };
}
test("cached projection and append-only delta stay inside the hot-path budget", () => {
const index = new SegmentIndex("perf");
index.hydrate([], DEFAULT_CONFIG);
const messages = Array.from({ length: 10_000 }, (_, value) => message(value));
const params = { environmentHash: "env", environmentTokens: 2_000, contextWindow: 300_000 };
index.project(messages, params);
const cacheStart = performance.now();
for (let iteration = 0; iteration < 1_000; iteration++) index.project(messages, params);
const cacheAverageMs = (performance.now() - cacheStart) / 1_000;
const appended = [...messages, message(10_000)];
const deltaStart = performance.now();
index.project(appended, params);
const deltaMs = performance.now() - deltaStart;
assert.ok(cacheAverageMs < 2, `cache-hit average ${cacheAverageMs.toFixed(3)}ms exceeded 2ms`);
assert.ok(deltaMs < 10, `delta projection ${deltaMs.toFixed(3)}ms exceeded 10ms`);
});
+84
View File
@@ -0,0 +1,84 @@
import test from "node:test";
import assert from "node:assert/strict";
import { DEFAULT_CONFIG } from "../src/config.ts";
import { SegmentIndex, deterministicSummary } from "../src/segment-index.ts";
const assistant = {
role: "assistant",
content: [{ type: "toolCall", id: "call-1", name: "bash", arguments: { command: "npm test" } }],
timestamp: 10,
};
const result = {
role: "toolResult",
toolCallId: "call-1",
toolName: "bash",
content: [{ type: "text", text: `failure\n${"x".repeat(12_000)}\nstack tail` }],
isError: true,
timestamp: 11,
};
test("deterministic summary is bounded and preserves head/tail evidence", () => {
const summary = deterministicSummary({
toolName: "bash",
args: { command: "npm test" },
rawText: result.content[0]!.text,
isError: true,
maxStubTokens: 256,
});
assert.match(summary, /tool=bash status=error/);
assert.match(summary, /failure/);
assert.match(summary, /stack tail/);
assert.ok(summary.length <= 1_100);
});
test("projection incrementally replaces tool results and caches identical input", () => {
const index = new SegmentIndex("session-1");
index.hydrate([], DEFAULT_CONFIG);
const records = index.captureTurn(assistant, [result], DEFAULT_CONFIG);
assert.equal(records.length, 1);
const messages = [assistant, result];
const first = index.project(messages, {
environmentHash: "env",
environmentTokens: 100,
contextWindow: 300_000,
});
const second = index.project(messages, {
environmentHash: "env",
environmentTokens: 100,
contextWindow: 300_000,
});
assert.strictEqual(first, second);
assert.ok(first.effectiveTokens < first.rawTokens);
assert.match(first.messages[1]!.content[0].text, /context_tree_query/);
const record = records[0]!;
assert.ok(index.applySemantic(record.sourceKey, record.sourceHash, "- npm test failed\n- stack tail"));
const semantic = index.project(messages, {
environmentHash: "env",
environmentTokens: 100,
contextWindow: 300_000,
});
assert.notStrictEqual(semantic, first);
assert.match(semantic.messages[1]!.content[0].text, /npm test failed/);
});
test("usage-backed baseline is adjusted only by projected delta", () => {
const index = new SegmentIndex("session-2");
index.hydrate([], DEFAULT_CONFIG);
const initial = index.project([assistant], {
environmentHash: "env",
environmentTokens: 50,
contextWindow: 100_000,
});
index.recordUsage(1_000, initial.messageEstimate, "env");
const next = index.project([assistant, { role: "user", content: "next", timestamp: 12 }], {
environmentHash: "env",
environmentTokens: 50,
contextWindow: 100_000,
});
assert.equal(next.confidence, "usage-backed");
assert.ok(next.effectiveTokens >= 1_000);
assert.ok(next.effectiveTokens < 1_100);
});
@@ -0,0 +1,19 @@
import test from "node:test";
import assert from "node:assert/strict";
import { shouldCompact } from "../src/token-accounting.ts";
test("raw pressure cannot trigger compaction while effective lower-bound stays below 90%", () => {
const config = { hardRatio: 0.9 };
const snapshot = { lowerBound: 231_000, contextWindow: 300_000 };
assert.equal(shouldCompact("threshold", snapshot, config), false);
assert.equal(shouldCompact("manual", snapshot, config), true);
assert.equal(shouldCompact("overflow", snapshot, config), true);
});
test("effective lower-bound at the configured threshold compacts", () => {
assert.equal(shouldCompact(
"threshold",
{ lowerBound: 270_000, contextWindow: 300_000 },
{ hardRatio: 0.9 },
), true);
});