Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 41 additions & 29 deletions apps/server/src/orchestration-v2/threadHistoryPaging.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,30 +155,21 @@ function makeProjection(visibleTurnItems: OrchestrationV2ProjectedTurnItem[]) {
}

describe("threadHistoryPaging", () => {
it("keeps a human turn whole up to the turn row budget and pages a longer one from inside", () => {
const humanTurn = (length: number) => {
const items = Array.from({ length }, (_, index) => makeRow(index));
items[0] = {
...items[0]!,
item: {
...items[0]!.item,
type: "user_message",
createdBy: "user",
creationSource: "web",
inputIntent: "turn_start",
messageId: MessageId.make("long-human-turn"),
text: "Continue",
attachments: [],
},
} as OrchestrationV2ProjectedTurnItem;
return items;
};

const whole = selectRecentTimelineWindow({ items: humanTurn(180), snapshotSequence: 1 });
expect(whole.items).toHaveLength(180);
expect(whole.hasMoreHistory).toBe(false);

const items = humanTurn(THREAD_HISTORY_PAGE_POLICY.maxTurnItems + 100);
it("bounds a single human turn and pages every item exactly once", () => {
const items = Array.from({ length: 180 }, (_, index) => makeRow(index));
items[0] = {
...items[0]!,
item: {
...items[0]!.item,
type: "user_message",
createdBy: "user",
creationSource: "web",
inputIntent: "turn_start",
messageId: MessageId.make("long-human-turn"),
text: "Continue",
attachments: [],
},
} as OrchestrationV2ProjectedTurnItem;
const recent = selectRecentTimelineWindow({ items, snapshotSequence: 1 });
expect(recent.items).toHaveLength(THREAD_HISTORY_PAGE_POLICY.maxItems);
const loaded = [...recent.items];
Expand All @@ -187,11 +178,7 @@ describe("threadHistoryPaging", () => {
expect(pages).toBeLessThan(items.length);
const page = selectHistoryPageFromCursor({ items, cursor, snapshotSequence: 1 });
expect(page.items.length).toBeGreaterThan(0);
expect(page.items.length).toBeLessThanOrEqual(
page.items[0]?.item.type === "user_message"
? THREAD_HISTORY_PAGE_POLICY.maxTurnItems
: THREAD_HISTORY_PAGE_POLICY.maxItems,
);
expect(page.items.length).toBeLessThanOrEqual(THREAD_HISTORY_PAGE_POLICY.maxItems);
loaded.unshift(...page.items);
cursor = page.nextCursor;
}
Expand All @@ -214,6 +201,31 @@ describe("threadHistoryPaging", () => {
);
});

it("honors a stricter maxTurnItems on human-turn windows", () => {
const items = Array.from({ length: 60 }, (_, index) => makeRow(index));
items[0] = {
...items[0]!,
item: {
...items[0]!.item,
type: "user_message",
createdBy: "user",
creationSource: "web",
inputIntent: "turn_start",
messageId: MessageId.make("stricter-turn-limit"),
text: "Continue",
attachments: [],
},
} as OrchestrationV2ProjectedTurnItem;

const recent = selectRecentTimelineWindow({
items,
snapshotSequence: 1,
policy: { ...THREAD_HISTORY_PAGE_POLICY, maxItems: 100, maxTurnItems: 20 },
});

expect(recent.items).toHaveLength(20);
});

it("keeps background turns with user turns, with main's 150-turn fan-out ceiling", () => {
const items = Array.from({ length: 161 }, (_, turn) => {
const row = makeRow(turn * 2);
Expand Down
12 changes: 6 additions & 6 deletions apps/server/src/orchestration-v2/threadHistoryPaging.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,8 @@ import type {
} from "@t3tools/contracts";

/**
* Every window stops at the byte budget. A window that holds a user turn also
* stops at `maxUserTurns` or `maxTurnItems` rows, which keeps ordinary turns
* whole (V1 kept 500 activities). Any other window, such as an agent-driven
* thread or the inside of one very long turn, stops at `maxItems` rows.
* Bound every history window by rows and bytes, including a single long human
* turn. Turn limits can stop a window earlier.
*/
export const THREAD_HISTORY_PAGE_POLICY = {
maxUserTurns: 10,
Expand Down Expand Up @@ -221,7 +219,9 @@ function selectOlderTimelinePage<Row extends HistoryRow>(input: {
? policy.maxUserTurns
: undefined;
const maxItems =
turnLimit === undefined ? policy.maxItems : (policy.maxTurnItems ?? policy.maxItems);
turnLimit === undefined
? policy.maxItems
: Math.min(policy.maxItems, policy.maxTurnItems ?? policy.maxItems);
for (let index = end - 1; index >= 0; index -= 1) {
const row = input.items[index]!;
const rowBytes = rowCost(row);
Expand All @@ -242,7 +242,7 @@ function selectOlderTimelinePage<Row extends HistoryRow>(input: {
// Inside a turn longer than the window, no selected row starts a user turn.
// Later pages and the snapshot query see only such rows and use `maxItems`,
// so this page uses it too.
if (userTurns === 0) selected.splice(policy.maxItems);
if (userTurns === 0) selected.splice(maxItems);
selected.reverse();

const oldest = selected[0];
Expand Down
Loading