From c2144779a30e17d65b1f0dcf2086a59151bcfd3d Mon Sep 17 00:00:00 2001 From: Yuruzuu Date: Sun, 1 Nov 2026 02:15:27 +0800 Subject: [PATCH] perf(server): stop summarizing side chat context at its cap --- .../ContextHandoffService.test.ts | 65 +++++++++++++------ .../orchestration-v2/ContextHandoffService.ts | 26 ++++---- 2 files changed, 58 insertions(+), 33 deletions(-) diff --git a/apps/server/src/orchestration-v2/ContextHandoffService.test.ts b/apps/server/src/orchestration-v2/ContextHandoffService.test.ts index d9005e6b6402e..b5109cb99d930 100644 --- a/apps/server/src/orchestration-v2/ContextHandoffService.test.ts +++ b/apps/server/src/orchestration-v2/ContextHandoffService.test.ts @@ -1,5 +1,6 @@ import { assert, it } from "@effect/vitest"; import { + ContextTransferId, MessageId, ProjectId, ProviderInstanceId, @@ -160,34 +161,56 @@ it.layer(layerTest)("ContextHandoffService legacy import", (it) => { ); }); -it("caps the side chat context block, keeping the newest parent items", () => { - const parentThreadId = ThreadId.make("thread:side-chat-parent"); - const digest = Array.from({ length: 60 }, (_, index) => - ContextHandoffService.summarizeDeltaItem( - importedItem({ +it.effect("caps the side chat context block without summarizing discarded parent history", () => + Effect.gen(function* () { + const parentThreadId = ThreadId.make("thread:side-chat-parent"); + let summarizedItems = 0; + const parentItems = Array.from({ length: 60 }, (_, index) => ({ + ...importedItem({ role: index % 2 === 0 ? "user" : "assistant", id: `parent-${index}`, text: `item-${index} ${"x".repeat(300)}`, ordinal: index, }), - )!, - ); - const context = ContextHandoffService.makeSideChatContext({ parentThreadId, digest }); + get text() { + summarizedItems++; + return `item-${index} ${"x".repeat(300)}`; + }, + })); + const service = yield* ContextHandoffService.ContextHandoffServiceV2; + const handoff = yield* service.prepareSideChatContext({ + parentThreadId, + threadId: ThreadId.make("thread:side-chat"), + targetRunId: RunId.make("run:side-chat"), + transferId: ContextTransferId.make("transfer:side-chat"), + toProviderThreadId: ProviderThreadId.make("provider-thread:side-chat"), + fromProviderInstanceId: ProviderInstanceId.make("codex"), + toProviderInstanceId: ProviderInstanceId.make("codex"), + coveredRunOrdinals: { from: 1, to: 1 }, + parentItems, + createdAt: DateTime.makeUnsafe(0), + }); + const context = handoff.summaryText; + assert.isBelow(summarizedItems, parentItems.length); - assert.include(context, "- Earlier items truncated."); - assert.notInclude(context, "item-0 "); - assert.include(context, "- Assistant: item-59 "); - assert.isAtMost(context.length, 8_000); - assert.isTrue( - context.endsWith( - `Use t3_thread_read on thread ${parentThreadId} to see the main thread's live state.`, - ), - ); + assert.include(context, "- Earlier items truncated."); + assert.notInclude(context, "item-0 "); + assert.include(context, "- Assistant: item-59 "); + assert.isAtMost(context.length, 8_000); + assert.isTrue( + context.endsWith( + `Use t3_thread_read on thread ${parentThreadId} to see the main thread's live state.`, + ), + ); - const framingOnly = ContextHandoffService.makeSideChatContext({ parentThreadId, digest: [] }); - assert.isTrue(framingOnly.startsWith("You are in a side chat beside the main thread.")); - assert.notInclude(framingOnly, "Main thread activity"); -}); + const framingOnly = ContextHandoffService.makeSideChatContext({ + parentThreadId, + digest: [], + }); + assert.isTrue(framingOnly.startsWith("You are in a side chat beside the main thread.")); + assert.notInclude(framingOnly, "Main thread activity"); + }).pipe(Effect.provide(layerTest)), +); it("covers the run range of a very long delta", () => { const runs = [ diff --git a/apps/server/src/orchestration-v2/ContextHandoffService.ts b/apps/server/src/orchestration-v2/ContextHandoffService.ts index 67a474d963bb0..7eef0afefbc17 100644 --- a/apps/server/src/orchestration-v2/ContextHandoffService.ts +++ b/apps/server/src/orchestration-v2/ContextHandoffService.ts @@ -202,7 +202,7 @@ export const readForkHistoryItems = Effect.fn( ]; }); -export function summarizeDeltaItem(item: OrchestrationV2TurnItem): string | null { +function summarizeDeltaItem(item: OrchestrationV2TurnItem): string | null { switch (item.type) { case "user_message": return `- User: ${compactText(item.text)}`; @@ -244,10 +244,10 @@ function makeForkDeltaSummary(input: { const SIDE_CHAT_CONTEXT_MAX_CHARS = 8_000; -/** `digest` holds `summarizeDeltaItem` lines for parent items after the fork point, oldest first. */ +/** Oldest first; raw parent items are summarized only until the context cap is reached. */ export function makeSideChatContext(input: { readonly parentThreadId: ThreadId; - readonly digest: ReadonlyArray; + readonly digest: ReadonlyArray; }): string { const framing = "You are in a side chat beside the main thread. The main thread may still be running in the same worktree. Answer the user's questions; do not continue or redo the main thread's task."; @@ -255,19 +255,24 @@ export function makeSideChatContext(input: { const truncated = "- Earlier items truncated."; const footer = `Use t3_thread_read on thread ${input.parentThreadId} to see the main thread's live state.`; const kept: Array = []; + let omitted = false; // The cap covers the whole block, so the fixed lines are reserved before any digest line. let remaining = SIDE_CHAT_CONTEXT_MAX_CHARS - [framing, "", header, truncated, "", footer].join("\n").length; - for (const line of input.digest.toReversed()) { - if (line.length + 1 > remaining) break; + for (let index = input.digest.length - 1; index >= 0; index--) { + const item = input.digest[index]!; + const line = typeof item === "string" ? item : summarizeDeltaItem(item); + if (line === null) continue; + if (line.length + 1 > remaining) { + omitted = true; + break; + } kept.unshift(line); remaining -= line.length + 1; } return [ framing, - ...(kept.length === 0 - ? [] - : ["", header, ...(kept.length < input.digest.length ? [truncated] : []), ...kept]), + ...(kept.length === 0 ? [] : ["", header, ...(omitted ? [truncated] : []), ...kept]), "", footer, ].join("\n"); @@ -588,10 +593,7 @@ const makeContextHandoffService = Effect.fn("orchestrationV2.ContextHandoffServi summaryMessageId: null, summaryText: makeSideChatContext({ parentThreadId: input.parentThreadId, - digest: input.parentItems.flatMap((item) => { - const line = summarizeDeltaItem(item); - return line === null ? [] : [line]; - }), + digest: input.parentItems, }), createdByProviderInstanceId: null, createdAt: input.createdAt,