From 5f43f30beccda1904ff8616a55567d083ad948f9 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sun, 25 Oct 2026 00:13:44 +0000 Subject: [PATCH 1/5] test(clients): detached threads hand off without a native thread ref Co-Authored-By: Claude Opus 5.5 --- .../src/state/threadWorkflows.test.ts | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/packages/client-runtime/src/state/threadWorkflows.test.ts b/packages/client-runtime/src/state/threadWorkflows.test.ts index e8e2c766b1ec..81252e3522d1 100644 --- a/packages/client-runtime/src/state/threadWorkflows.test.ts +++ b/packages/client-runtime/src/state/threadWorkflows.test.ts @@ -62,7 +62,22 @@ describe("thread workflows", () => { ...projection, providerThreads: [{ ...projection.providerThreads[0]!, nativeThreadRef: null }], }), - ).toBe(false); + ).toBe(true); + + const attached = (supportsProviderSwitchingViaHandoff: boolean) => + threadSupportsProviderHandoff({ + ...projection, + providerThreads: [{ ...projection.providerThreads[0]!, nativeThreadRef: null }], + providerSessions: [ + { + id: "detached-session", + status: "ready", + capabilities: { sessions: { supportsProviderSwitchingViaHandoff } }, + }, + ], + } as unknown as OrchestrationV2ThreadProjection); + expect(attached(true)).toBe(true); + expect(attached(false)).toBe(false); }); it("sorts queued messages and gates reorder and promotion from capabilities", () => { From b234eb03727ac581cc947b488176a551b5769f40 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sun, 25 Oct 2026 00:16:06 +0000 Subject: [PATCH 2/5] fix(clients): detached threads can switch providers without a native thread ref Co-Authored-By: Claude Opus 5.5 --- .../client-runtime/src/state/threadWorkflows.ts | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/packages/client-runtime/src/state/threadWorkflows.ts b/packages/client-runtime/src/state/threadWorkflows.ts index 21139a2c4549..2a0092402adf 100644 --- a/packages/client-runtime/src/state/threadWorkflows.ts +++ b/packages/client-runtime/src/state/threadWorkflows.ts @@ -94,18 +94,9 @@ export function threadSupportsProviderHandoff(projection: Projection | null | un if (session !== null) { return session.capabilities.sessions.supportsProviderSwitchingViaHandoff; } - if (resolveActiveThreadRun(projection) !== null) return false; - if (projection.thread.historyOrigin === "v1_import" || projection.runs.length === 0) return true; - - // Detaching a stopped session removes it from the projection, but its native - // provider thread remains available for the next turn's portable handoff. - return projection.providerThreads.some( - (thread) => - thread.id === projection.thread.activeProviderThreadId && - thread.appThreadId === projection.thread.id && - thread.providerInstanceId === projection.thread.modelSelection.instanceId && - thread.nativeThreadRef !== null, - ); + // Without a live session the server hands off from saved app history, so the + // outgoing provider's native thread ref is not required. + return resolveActiveThreadRun(projection) === null; } export function getUserQueuedThreadRuns( From 63c3d7155cc33d1f701b867ab41527ceae6e085b Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sun, 25 Oct 2026 00:22:11 +0000 Subject: [PATCH 3/5] test(clients): provider handoff still needs the source provider thread Co-Authored-By: Claude Opus 5.5 --- packages/client-runtime/src/state/threadWorkflows.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/client-runtime/src/state/threadWorkflows.test.ts b/packages/client-runtime/src/state/threadWorkflows.test.ts index 81252e3522d1..12277f710dc4 100644 --- a/packages/client-runtime/src/state/threadWorkflows.test.ts +++ b/packages/client-runtime/src/state/threadWorkflows.test.ts @@ -63,6 +63,7 @@ describe("thread workflows", () => { providerThreads: [{ ...projection.providerThreads[0]!, nativeThreadRef: null }], }), ).toBe(true); + expect(threadSupportsProviderHandoff({ ...projection, providerThreads: [] })).toBe(false); const attached = (supportsProviderSwitchingViaHandoff: boolean) => threadSupportsProviderHandoff({ From cf8de167ae5477317b3bb4e942a802b23d401ee2 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sun, 25 Oct 2026 00:23:01 +0000 Subject: [PATCH 4/5] fix(clients): keep the source provider thread guard for detached handoffs Co-Authored-By: Claude Opus 5.5 --- .../client-runtime/src/state/threadWorkflows.ts | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/packages/client-runtime/src/state/threadWorkflows.ts b/packages/client-runtime/src/state/threadWorkflows.ts index 2a0092402adf..dbadc63db61f 100644 --- a/packages/client-runtime/src/state/threadWorkflows.ts +++ b/packages/client-runtime/src/state/threadWorkflows.ts @@ -94,9 +94,18 @@ export function threadSupportsProviderHandoff(projection: Projection | null | un if (session !== null) { return session.capabilities.sessions.supportsProviderSwitchingViaHandoff; } - // Without a live session the server hands off from saved app history, so the - // outgoing provider's native thread ref is not required. - return resolveActiveThreadRun(projection) === null; + if (resolveActiveThreadRun(projection) !== null) return false; + if (projection.thread.historyOrigin === "v1_import" || projection.runs.length === 0) return true; + + // Detaching a stopped session removes it from the projection. The server still + // hands off from saved app history, which needs the source provider thread but + // not its native ref. + return projection.providerThreads.some( + (thread) => + thread.id === projection.thread.activeProviderThreadId && + thread.appThreadId === projection.thread.id && + thread.providerInstanceId === projection.thread.modelSelection.instanceId, + ); } export function getUserQueuedThreadRuns( From 4cef96c259b3a1a99c41ab341f291525da0fc296 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sat, 14 Nov 2026 07:30:00 +0000 Subject: [PATCH 5/5] test(clients): keep only the detached handoff cases Co-Authored-By: Claude Opus 5.5 --- .../src/state/threadWorkflows.test.ts | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/packages/client-runtime/src/state/threadWorkflows.test.ts b/packages/client-runtime/src/state/threadWorkflows.test.ts index b1038c11422d..3fe571e84b5b 100644 --- a/packages/client-runtime/src/state/threadWorkflows.test.ts +++ b/packages/client-runtime/src/state/threadWorkflows.test.ts @@ -64,21 +64,6 @@ describe("thread workflows", () => { }), ).toBe(true); expect(threadSupportsProviderHandoff({ ...projection, providerThreads: [] })).toBe(false); - - const attached = (supportsProviderSwitchingViaHandoff: boolean) => - threadSupportsProviderHandoff({ - ...projection, - providerThreads: [{ ...projection.providerThreads[0]!, nativeThreadRef: null }], - providerSessions: [ - { - id: "detached-session", - status: "ready", - capabilities: { sessions: { supportsProviderSwitchingViaHandoff } }, - }, - ], - } as unknown as OrchestrationV2ThreadProjection); - expect(attached(true)).toBe(true); - expect(attached(false)).toBe(false); }); it("sorts queued messages and gates promotion from capabilities", () => {