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(
4040 group: "orchestration",
4141 method: "GET",
4242 url: (httpBaseUrl) =>
43 environmentEndpointUrl(httpBaseUrl, `/api/orchestration/threads/${input.threadId}/bounded`),
43 environmentEndpointUrl(
44 httpBaseUrl,
45 `/api/orchestration/threads/${encodeURIComponent(input.threadId)}/bounded`,
46 ),
4447 timeoutMs: input.timeoutMs ?? DEFAULT_BOUNDED_THREAD_SNAPSHOT_TIMEOUT_MS,
4548 request: ({ client, headers }) =>
4649 client.threadBoundedSnapshot({
packages/client-runtime/src/state/environmentHttpAuth.test.ts+45
......@@ -4,6 +4,7 @@ import {
44 ORCHESTRATION_PROTOCOL_HEADER,
55 ORCHESTRATION_PROTOCOL_VERSION_TEXT,
66 ProjectId,
7 ThreadId,
78 type AuthSessionState,
89 type OrchestrationV2ShellSnapshot,
910 OrchestrationV2ThreadDetailSnapshot,
......@@ -307,6 +308,50 @@ describe("authenticated environment HTTP requests", () => {
307308 }),
308309 );
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
310355 it.effect("retries a rejected diff once with a new token, endpoint, and proof", () =>
311356 Effect.gen(function* () {
312357 const harness = makeHarness((requestNumber) =>
packages/client-runtime/src/state/threadHistoryHttp.ts+4-1
......@@ -30,7 +30,10 @@ export const fetchEnvironmentThreadHistoryPage = Effect.fn(
3030 group: "orchestration",
3131 method: "GET",
3232 url: (httpBaseUrl) =>
33 environmentEndpointUrl(httpBaseUrl, `/api/orchestration/threads/${input.threadId}/history`),
33 environmentEndpointUrl(
34 httpBaseUrl,
35 `/api/orchestration/threads/${encodeURIComponent(input.threadId)}/history`,
36 ),
3437 timeoutMs: input.timeoutMs ?? DEFAULT_THREAD_HISTORY_TIMEOUT_MS,
3538 request: ({ client, headers }) =>
3639 client.threadHistoryPage({
packages/client-runtime/src/state/threadSnapshotHttp.ts+4-1
......@@ -66,7 +66,10 @@ export const fetchEnvironmentThreadSnapshot = Effect.fn(
6666 group: "orchestration",
6767 method: "GET",
6868 url: (httpBaseUrl) =>
69 environmentEndpointUrl(httpBaseUrl, `/api/orchestration/threads/${input.threadId}`),
69 environmentEndpointUrl(
70 httpBaseUrl,
71 `/api/orchestration/threads/${encodeURIComponent(input.threadId)}`,
72 ),
7073 timeoutMs: input.timeoutMs ?? DEFAULT_THREAD_SNAPSHOT_TIMEOUT_MS,
7174 request: ({ client, headers }) =>
7275 client.threadSnapshot({