From 3273abd64f222961acddb0059edc0b4e9a7db7e5 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Thu, 12 Nov 2026 07:37:46 +0000 Subject: [PATCH 1/2] test(server): returning to a provider keeps a native session the byte estimate calls full Reproduces #23142: with no reported window or usage, about 112 KB of ordinary prose (about 25k tokens) is counted as 112k tokens against the 128k fallback window, so T3 replaces the native session. AI-assisted; verified with the tests and tools listed above. --- .../ProviderSwitch.integration.test.ts | 100 ++++++++++++++++++ 1 file changed, 100 insertions(+) diff --git a/apps/server/src/orchestration-v2/testkit/ProviderSwitch.integration.test.ts b/apps/server/src/orchestration-v2/testkit/ProviderSwitch.integration.test.ts index f252380bf7f70..8df19e1b8f3a4 100644 --- a/apps/server/src/orchestration-v2/testkit/ProviderSwitch.integration.test.ts +++ b/apps/server/src/orchestration-v2/testkit/ProviderSwitch.integration.test.ts @@ -1230,6 +1230,106 @@ describe("orchestration v2 provider switching", () => { ), ); + it.live("keeps a native session whose stored prose is far below an unreported window", () => + Effect.scoped( + Effect.gen(function* () { + const cwd = yield* checkpointWorkspace("handoff-estimate-headroom"); + const capturedTurns = yield* Ref.make>([]); + const generation = yield* Ref.make(0); + // About 112 KB of ordinary English is about 25k tokens: a quarter of a 128k window. + const prose = "The quick brown fox jumps over the lazy dog. ".repeat(2_500); + const layerRegistry = ProviderAdapterRegistry.layerFromAdapters([ + makeTestAdapter({ + instanceId: CODEX_MODEL_SELECTION.instanceId, + driver: CODEX_DRIVER, + capabilities: CodexProviderCapabilitiesV2, + modelSelection: CODEX_MODEL_SELECTION, + responseByRunOrdinal: { 2: "Source work" }, + capturedTurns, + }), + // No window and no usage, like an ACP registry agent that sends no usage_update. + makeTestAdapter({ + instanceId: CLAUDE_MODEL_SELECTION.instanceId, + driver: CLAUDE_DRIVER, + capabilities: ClaudeProviderCapabilitiesV2, + modelSelection: CLAUDE_MODEL_SELECTION, + responseByRunOrdinal: { 1: prose }, + capturedTurns, + nativeThreadGeneration: generation, + }), + ]); + yield* Effect.gen(function* () { + const orchestrator = yield* Orchestrator.OrchestratorV2; + const worker = yield* EffectWorker.OrchestrationEffectWorkerV2; + const dispatch = (ordinal: number, text: string, selection: ModelSelection) => + orchestrator.dispatch({ + type: "message.dispatch", + commandId: CommandId.make(`headroom:${ordinal}`), + threadId, + messageId: MessageId.make(`headroom:${ordinal}`), + createdBy: "user", + creationSource: "web", + text, + attachments: [], + modelSelection: selection, + dispatchMode: { type: "start_immediately" }, + }); + const wait = (ordinal: number) => + orchestrator.streamStoredEvents.pipe( + Stream.filter( + ({ event }) => + event.type === "run.updated" && + event.payload.ordinal === ordinal && + (event.payload.status === "completed" || event.payload.status === "failed"), + ), + Stream.runHead, + Effect.andThen(worker.drain()), + ); + yield* orchestrator.dispatch({ + type: "thread.create", + commandId: CommandId.make("headroom:create"), + threadId, + projectId, + createdBy: "user", + creationSource: "web", + title: "Handoff estimate headroom", + modelSelection: CLAUDE_MODEL_SELECTION, + runtimeMode: "full-access", + interactionMode: "default", + branch: null, + worktreePath: null, + }); + yield* dispatch(1, "Write the long report", CLAUDE_MODEL_SELECTION); + yield* wait(1); + yield* dispatch(2, "Continue elsewhere", CODEX_MODEL_SELECTION); + yield* wait(2); + yield* dispatch(3, "Back to the first provider", CLAUDE_MODEL_SELECTION); + yield* wait(3); + assert.equal( + (yield* orchestrator.getThreadProjection(threadId)).runs.at(-1)?.status, + "completed", + ); + // A second generation means T3 discarded the native session as full. + assert.equal(yield* Ref.get(generation), 1); + }).pipe( + Effect.provide( + ProviderReplayHarness.layerWithRegistry( + { + name: "handoff-estimate-headroom", + runtimePolicyOverride: { + cwd, + approvalPolicy: "never", + sandboxPolicy: { type: "readOnly" }, + }, + }, + layerRegistry, + ), + ), + ); + }), + ), + ); + it.live.each(["native", "legacy"] as const)( "starts a replacement native thread as new after the resume fallback with %s attempts", (attempts) => From c2c187dc6750da2ae6eb2f782b61adde506dbf96 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Thu, 12 Nov 2026 07:44:56 +0000 Subject: [PATCH 2/2] fix(server): handoff budget estimates stored history in tokens, not bytes When a provider reports no usage, T3 estimated the native session's occupancy by counting every stored history byte as one token. About 100 KB of history (around 30k real tokens) then looked full against a 128k window, and switching back replaced the agent's native session with a fresh one carrying only a bounded handoff. Estimate that occupancy at ceil(UTF-8 bytes / 4), the same rule Codex uses for its own count. The new turn and imported history keep their pessimistic byte counts. Fixes pingdotgg/t3code#23142 AI-assisted; verified with the tests and tools listed above. --- .../src/orchestration-v2/ProviderTurnStartService.ts | 3 ++- .../testkit/ProviderSwitch.integration.test.ts | 2 +- packages/provider-core/src/server/handoffBudget.ts | 7 ++++++- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/apps/server/src/orchestration-v2/ProviderTurnStartService.ts b/apps/server/src/orchestration-v2/ProviderTurnStartService.ts index a506cc6e122c6..cf52173f45c6a 100644 --- a/apps/server/src/orchestration-v2/ProviderTurnStartService.ts +++ b/apps/server/src/orchestration-v2/ProviderTurnStartService.ts @@ -45,6 +45,7 @@ import { handoffTokenCapConfig, handoffBudget, attachmentTokenAllowance, + estimateTextTokens, contextUsageForHandoff, historicalMessage, latestNativeContextUsage, @@ -998,7 +999,7 @@ export const layer: Layer.Layer< : 0; return ( sum + - (historical === null ? 0 : Buffer.byteLength(historical.text)) + + (historical === null ? 0 : estimateTextTokens(historical.text)) + nativeAttachments ); }, 0) diff --git a/apps/server/src/orchestration-v2/testkit/ProviderSwitch.integration.test.ts b/apps/server/src/orchestration-v2/testkit/ProviderSwitch.integration.test.ts index 8df19e1b8f3a4..306935cdd0975 100644 --- a/apps/server/src/orchestration-v2/testkit/ProviderSwitch.integration.test.ts +++ b/apps/server/src/orchestration-v2/testkit/ProviderSwitch.integration.test.ts @@ -510,7 +510,7 @@ describe("orchestration v2 provider switching", () => { capabilities: ClaudeProviderCapabilitiesV2, modelSelection: CLAUDE_MODEL_SELECTION, responseByRunOrdinal: scenario.includes("target-estimate") - ? { 2: "x".repeat(10_000) } + ? { 2: "x".repeat(64_000) } : {}, capturedTurns, ...(scenario.includes("retry") || scenario.includes("unsent") ? { failStartOnce } : {}), diff --git a/packages/provider-core/src/server/handoffBudget.ts b/packages/provider-core/src/server/handoffBudget.ts index 5f9f2f3ae33f6..735647693d06d 100644 --- a/packages/provider-core/src/server/handoffBudget.ts +++ b/packages/provider-core/src/server/handoffBudget.ts @@ -95,7 +95,12 @@ export function attachmentTokenAllowance(attachments: ReadonlyArray