Session history was silently clipped to the first 4000 characters of the last 12 messages, so follow-ups could not see timing or audit tables. Keep an append-only tail plus a checkpoint summary, retry overflow in the same request, and expose cache hit rate in admin usage. Co-authored-by: Cursor <cursoragent@cursor.com>
113 lines
4.5 KiB
TypeScript
113 lines
4.5 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import test from "node:test";
|
|
|
|
import {
|
|
CONSULTATION_HISTORY_MESSAGE_CHARS,
|
|
clipConsultationHistoryText,
|
|
consultationHistoryFromStoredMessages,
|
|
consultationHistoryWindow,
|
|
consultationUserTurnContent,
|
|
historyBudgetChars,
|
|
isContextOverflowError,
|
|
omissionMarker,
|
|
SESSION_CONTEXT_SUMMARY_HEADING,
|
|
} from "../src/lib/consultation-session-history.ts";
|
|
|
|
function numberedMessages(count: number, text: (index: number) => string) {
|
|
return Array.from({ length: count }, (_, index) => ({
|
|
role: index % 2 === 0 ? "user" : "assistant",
|
|
text: text(index),
|
|
requestId: `req-${index}`,
|
|
}));
|
|
}
|
|
|
|
test("20 unspecialized messages at 128k drop whole oldest turns to fit the budget", () => {
|
|
const body = "x".repeat(3_000);
|
|
const history = consultationHistoryWindow(numberedMessages(20, () => body), null, {
|
|
contextWindow: 128_000,
|
|
});
|
|
const budget = historyBudgetChars(128_000);
|
|
assert.equal(budget, 40_000);
|
|
assert.ok(history.droppedCount > 0);
|
|
assert.equal(history.tail.length, 20 - history.droppedCount);
|
|
assert.equal(history.tail[0]?.text, body);
|
|
assert.ok(history.tail.reduce((sum, message) => sum + message.text.length, 0) <= budget);
|
|
assert.equal(history.summaryText, null);
|
|
});
|
|
|
|
test("a summary throughMessageIndex keeps only later turns in the tail", () => {
|
|
const messages = numberedMessages(20, (index) => `keep-${index}`);
|
|
const history = consultationHistoryWindow(messages, {
|
|
version: 1,
|
|
text: "先前结论",
|
|
throughRequestId: "req-14",
|
|
throughMessageIndex: 14,
|
|
messageCount: 15,
|
|
updatedAt: "2026-09-06T00:00:00.000Z",
|
|
}, { contextWindow: 128_000 });
|
|
|
|
assert.deepEqual(history.tail.map((message) => message.text), [
|
|
"keep-15",
|
|
"keep-16",
|
|
"keep-17",
|
|
"keep-18",
|
|
"keep-19",
|
|
]);
|
|
assert.equal(history.summaryText, "先前结论");
|
|
assert.equal(history.droppedCount, 0);
|
|
});
|
|
|
|
test("a 20_000 character message is clipped to 12_000 with an omission count", () => {
|
|
const text = "y".repeat(20_000);
|
|
const clipped = clipConsultationHistoryText(text);
|
|
assert.equal(clipped.startsWith("y".repeat(CONSULTATION_HISTORY_MESSAGE_CHARS)), true);
|
|
assert.equal(clipped.includes(omissionMarker(8_000)), true);
|
|
assert.equal(clipped.length, CONSULTATION_HISTORY_MESSAGE_CHARS + omissionMarker(8_000).length);
|
|
|
|
const history = consultationHistoryWindow([
|
|
{ role: "assistant", text },
|
|
], null, { contextWindow: 128_000 });
|
|
assert.equal(history.tail[0]?.text, clipped);
|
|
});
|
|
|
|
test("historyBudgetChars uses 64k, 32k, and null-as-128k windows", () => {
|
|
assert.equal(historyBudgetChars(64_000), 6_000);
|
|
assert.equal(historyBudgetChars(32_000), 4_000);
|
|
assert.equal(historyBudgetChars(null), historyBudgetChars(128_000));
|
|
assert.equal(historyBudgetChars(undefined), 40_000);
|
|
});
|
|
|
|
test("stored consultation history can skip the in-flight request id", () => {
|
|
const history = consultationHistoryFromStoredMessages([
|
|
{ role: "user", text: "old", requestId: "keep" },
|
|
{ role: "user", text: "current", requestId: "skip" },
|
|
], { excludeRequestId: "skip" });
|
|
|
|
assert.deepEqual(history, [{ role: "user", text: "old" }]);
|
|
});
|
|
|
|
test("the summary block sits after the time line and before the question", () => {
|
|
const content = consultationUserTurnContent({
|
|
currentTime: "当前时间:2026-09-06 14:00(中国)",
|
|
name: "测",
|
|
instruction: "先加载 Jyotish Skill",
|
|
summaryText: "上次应期在 2027 年",
|
|
question: "你前面说的应期是哪年",
|
|
});
|
|
const timeAt = content.indexOf("当前时间:");
|
|
const summaryAt = content.indexOf(SESSION_CONTEXT_SUMMARY_HEADING);
|
|
const questionAt = content.indexOf("你前面说的应期是哪年");
|
|
assert.ok(timeAt >= 0 && summaryAt > timeAt && questionAt > summaryAt);
|
|
});
|
|
|
|
test("context overflow detection covers provider codes and related 400s", () => {
|
|
assert.equal(isContextOverflowError({ code: "context_length_exceeded" }), true);
|
|
assert.equal(isContextOverflowError(new Error("maximum context length exceeded")), true);
|
|
assert.equal(isContextOverflowError({ message: "prompt is too long" }), true);
|
|
assert.equal(isContextOverflowError({ message: "input is too long" }), true);
|
|
assert.equal(isContextOverflowError({ message: "too many tokens in the prompt" }), true);
|
|
assert.equal(isContextOverflowError({ status: 400, message: "max_tokens for this context" }), true);
|
|
assert.equal(isContextOverflowError({ status: 400, message: "invalid json" }), false);
|
|
assert.equal(isContextOverflowError(new Error("rate limit")), false);
|
|
});
|