From b80baad8e5eaaec90f02bb5eaf795bd25d3c793f Mon Sep 17 00:00:00 2001 From: Shivam Sharma <91240327+shivamhwp@users.noreply.github.com> Date: Thu, 5 Nov 2026 08:27:20 +0530 Subject: [PATCH] fix(server): a stopped run no longer shrinks thread history to one row #26435 dropped the half-budget floor for the visible window whenever any retained dependency card exists, and every stored run_interrupt_request counts as one. Threads with more than ~512 KiB of control state then loaded a single history row. The floor now stays, minus the dependency reserve, so large pending question cards still fit. Co-Authored-By: Claude Opus 5.5 --- .../threadHistoryPaging.test.ts | 69 ++++++++++--------- .../orchestration-v2/threadHistoryPaging.ts | 4 +- 2 files changed, 40 insertions(+), 33 deletions(-) diff --git a/apps/server/src/orchestration-v2/threadHistoryPaging.test.ts b/apps/server/src/orchestration-v2/threadHistoryPaging.test.ts index eba4ceec7f6c8..111cf0541358a 100644 --- a/apps/server/src/orchestration-v2/threadHistoryPaging.test.ts +++ b/apps/server/src/orchestration-v2/threadHistoryPaging.test.ts @@ -904,39 +904,46 @@ describe("threadHistoryPaging", () => { expect(bounded.projection.visibleTurnItems).toEqual(actionable.visibleTurnItems); }); - it("keeps agent-only turns visible when checkpoint file lists fill the byte budget", () => { - const items = Array.from({ length: 3 }, (_, turn) => { - const first = makeRow(turn * 5); - const prompt = { - ...first, - item: { - ...first.item, - type: "user_message" as const, - createdBy: "agent" as const, - creationSource: "provider" as const, - inputIntent: "turn_start" as const, - messageId: MessageId.make(`agent-prompt-${turn}`), - text: `Step ${turn}`, - attachments: [], - }, - } as OrchestrationV2ProjectedTurnItem; - return [prompt, ...Array.from({ length: 4 }, (_, index) => makeRow(turn * 5 + index + 1))]; - }).flat(); - const files = Array.from( - { length: 15_000 }, - (_, index) => - `work/ws-${index % 40}/.jj/repo/store/extra/${index.toString(16).padStart(40, "0")}`, - ); - const projection = { - ...makeProjection(items), - checkpoints: [{ id: "checkpoint-1", files }], - } as unknown as ReturnType; + it.each([false, true])( + "keeps agent-only turns visible when checkpoint file lists fill the byte budget (stored interrupt request: %s)", + (withInterruptRequest) => { + const items = Array.from({ length: 3 }, (_, turn) => { + const first = makeRow(turn * 5); + const prompt = { + ...first, + item: { + ...first.item, + type: "user_message" as const, + createdBy: "agent" as const, + creationSource: "provider" as const, + inputIntent: "turn_start" as const, + messageId: MessageId.make(`agent-prompt-${turn}`), + text: `Step ${turn}`, + attachments: [], + }, + } as OrchestrationV2ProjectedTurnItem; + return [prompt, ...Array.from({ length: 4 }, (_, index) => makeRow(turn * 5 + index + 1))]; + }).flat(); + const files = Array.from( + { length: 15_000 }, + (_, index) => + `work/ws-${index % 40}/.jj/repo/store/extra/${index.toString(16).padStart(40, "0")}`, + ); + const base = makeProjection(items); + const projection = { + ...base, + turnItems: withInterruptRequest + ? [interruptItem("old-interrupt-req", "run_interrupt_request", 0), ...base.turnItems] + : base.turnItems, + checkpoints: [{ id: "checkpoint-1", files }], + } as unknown as ReturnType; - const bounded = buildBoundedThreadProjection({ projection, snapshotSequence: 1 }); + const bounded = buildBoundedThreadProjection({ projection, snapshotSequence: 1 }); - expect(bounded.projection.visibleTurnItems).toHaveLength(items.length); - expect(bounded.hasMoreHistory).toBe(false); - }); + expect(bounded.projection.visibleTurnItems).toHaveLength(items.length); + expect(bounded.hasMoreHistory).toBe(false); + }, + ); it("keeps paged historical plan detail in the turn item and only status in its artifact", () => { const detail = "Implement the historical plan exactly.\n".repeat(1_000); diff --git a/apps/server/src/orchestration-v2/threadHistoryPaging.ts b/apps/server/src/orchestration-v2/threadHistoryPaging.ts index d226d21e3248c..d96ef3c26b2cb 100644 --- a/apps/server/src/orchestration-v2/threadHistoryPaging.ts +++ b/apps/server/src/orchestration-v2/threadHistoryPaging.ts @@ -673,9 +673,9 @@ export function buildBoundedThreadProjection(input: { turnItems: [], visibleTurnItems: [], }); - // Keep the control-only window minimum, but required dependency cards claim their reserve first. + // Control state cannot be paged, so the window keeps half the budget; dependency cards take their reserve from that half. const windowBudget = Math.max( - dependencyReserve === 0 ? policy.maxEncodedBytes / 2 : 0, + Math.max(policy.maxEncodedBytes / 2 - dependencyReserve, 0), policy.maxEncodedBytes - dependencyReserve - controlBytes - 1_024, ); const windowPolicy: ThreadHistoryPagePolicy = { ...policy, maxEncodedBytes: windowBudget };