authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-10-09 13:29:41-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-10-09 20:29:41+00:00
loga1db449fe4fecafcb473340771163e791b7fb3d6
treec29ff9a3bc0276d6b5ee45ab0404c68082ffb596
parent454b94a13aea918f27bb060d8d054d4cfb791bc2
signature Signed by PGP key B5690EEEBB952194

fix(client): load earlier turns works for MCP threads over T3 Connect (#17599)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

4 files changed, 57 insertions(+), 3 deletions(-)

packages/client-runtime/src/state/boundedThreadSnapshotHttp.ts+4-1
...@@ -40,7 +40,10 @@ export const fetchEnvironmentBoundedThreadSnapshot = Effect.fn(...@@ -40,7 +40,10 @@ export const fetchEnvironmentBoundedThreadSnapshot = Effect.fn(
40 group: "orchestration",40 group: "orchestration",
41 method: "GET",41 method: "GET",
42 url: (httpBaseUrl) =>42 url: (httpBaseUrl) =>
43 environmentEndpointUrl(httpBaseUrl, `/api/orchestration/threads/${input.threadId}/bounded`),43 environmentEndpointUrl(
44 httpBaseUrl,
45 `/api/orchestration/threads/${encodeURIComponent(input.threadId)}/bounded`,
46 ),
44 timeoutMs: input.timeoutMs ?? DEFAULT_BOUNDED_THREAD_SNAPSHOT_TIMEOUT_MS,47 timeoutMs: input.timeoutMs ?? DEFAULT_BOUNDED_THREAD_SNAPSHOT_TIMEOUT_MS,
45 request: ({ client, headers }) =>48 request: ({ client, headers }) =>
46 client.threadBoundedSnapshot({49 client.threadBoundedSnapshot({
packages/client-runtime/src/state/environmentHttpAuth.test.ts+45
...@@ -4,6 +4,7 @@ import {...@@ -4,6 +4,7 @@ import {
4 ORCHESTRATION_PROTOCOL_HEADER,4 ORCHESTRATION_PROTOCOL_HEADER,
5 ORCHESTRATION_PROTOCOL_VERSION_TEXT,5 ORCHESTRATION_PROTOCOL_VERSION_TEXT,
6 ProjectId,6 ProjectId,
7 ThreadId,
7 type AuthSessionState,8 type AuthSessionState,
8 type OrchestrationV2ShellSnapshot,9 type OrchestrationV2ShellSnapshot,
9 OrchestrationV2ThreadDetailSnapshot,10 OrchestrationV2ThreadDetailSnapshot,
...@@ -307,6 +308,50 @@ describe("authenticated environment HTTP requests", () => {...@@ -307,6 +308,50 @@ describe("authenticated environment HTTP requests", () => {
307 }),308 }),
308 );309 );
309310
311 // MCP-created thread ids contain ":", which the request path percent-encodes.
312 // The DPoP proof must sign the URL that is actually sent, or the environment
313 // rejects it as a URL mismatch.
314 const MCP_THREAD_ID = ThreadId.make("mcp:3534bc83-1c17-4a1e-9118-601c2766d355");
315 const MCP_THREAD_LOADERS: ReadonlyArray<
316 Pick<(typeof LOADERS)[number], "name" | "response" | "load">
317 > = [
318 {
319 name: "thread snapshot",
320 response: encodeThreadSnapshot(THREAD),
321 load: (input: HttpInput) =>
322 ThreadSnapshotLoader.fetchEnvironmentThreadSnapshot({ ...input, threadId: MCP_THREAD_ID }),
323 },
324 {
325 name: "bounded thread snapshot",
326 response: encodeBoundedSnapshot(BOUNDED_THREAD),
327 load: (input: HttpInput) =>
328 fetchEnvironmentBoundedThreadSnapshot({ ...input, threadId: MCP_THREAD_ID }),
329 },
330 {
331 name: "older thread history",
332 response: THREAD_HISTORY,
333 load: (input: HttpInput) =>
334 fetchEnvironmentThreadHistoryPage({
335 ...input,
336 threadId: MCP_THREAD_ID,
337 cursor: "older-page",
338 }),
339 },
340 ];
341 it.effect.each(MCP_THREAD_LOADERS)(
342 "signs the sent URL for a $name of a thread id that needs encoding",
343 (loader) =>
344 Effect.gen(function* () {
345 const harness = makeHarness(() => Response.json(loader.response));
346 yield* loader.load(harness.input).pipe(Effect.provide(harness.httpLayer));
347
348 const sent = new URL(harness.calls[0]!.url);
349 expect(sent.pathname).toContain("/mcp%3A3534bc83-");
350 sent.search = "";
351 expect(harness.proofs.map((proof) => proof.url)).toEqual([sent.toString()]);
352 }),
353 );
354
310 it.effect("retries a rejected diff once with a new token, endpoint, and proof", () =>355 it.effect("retries a rejected diff once with a new token, endpoint, and proof", () =>
311 Effect.gen(function* () {356 Effect.gen(function* () {
312 const harness = makeHarness((requestNumber) =>357 const harness = makeHarness((requestNumber) =>
packages/client-runtime/src/state/threadHistoryHttp.ts+4-1
...@@ -30,7 +30,10 @@ export const fetchEnvironmentThreadHistoryPage = Effect.fn(...@@ -30,7 +30,10 @@ export const fetchEnvironmentThreadHistoryPage = Effect.fn(
30 group: "orchestration",30 group: "orchestration",
31 method: "GET",31 method: "GET",
32 url: (httpBaseUrl) =>32 url: (httpBaseUrl) =>
33 environmentEndpointUrl(httpBaseUrl, `/api/orchestration/threads/${input.threadId}/history`),33 environmentEndpointUrl(
34 httpBaseUrl,
35 `/api/orchestration/threads/${encodeURIComponent(input.threadId)}/history`,
36 ),
34 timeoutMs: input.timeoutMs ?? DEFAULT_THREAD_HISTORY_TIMEOUT_MS,37 timeoutMs: input.timeoutMs ?? DEFAULT_THREAD_HISTORY_TIMEOUT_MS,
35 request: ({ client, headers }) =>38 request: ({ client, headers }) =>
36 client.threadHistoryPage({39 client.threadHistoryPage({
packages/client-runtime/src/state/threadSnapshotHttp.ts+4-1
...@@ -66,7 +66,10 @@ export const fetchEnvironmentThreadSnapshot = Effect.fn(...@@ -66,7 +66,10 @@ export const fetchEnvironmentThreadSnapshot = Effect.fn(
66 group: "orchestration",66 group: "orchestration",
67 method: "GET",67 method: "GET",
68 url: (httpBaseUrl) =>68 url: (httpBaseUrl) =>
69 environmentEndpointUrl(httpBaseUrl, `/api/orchestration/threads/${input.threadId}`),69 environmentEndpointUrl(
70 httpBaseUrl,
71 `/api/orchestration/threads/${encodeURIComponent(input.threadId)}`,
72 ),
70 timeoutMs: input.timeoutMs ?? DEFAULT_THREAD_SNAPSHOT_TIMEOUT_MS,73 timeoutMs: input.timeoutMs ?? DEFAULT_THREAD_SNAPSHOT_TIMEOUT_MS,
71 request: ({ client, headers }) =>74 request: ({ client, headers }) =>
72 client.threadSnapshot({75 client.threadSnapshot({