From 28ee287df4fc9419335ca1905cc04d7721f15f96 Mon Sep 17 00:00:00 2001 From: Will Adams-Keane Date: Fri, 30 Oct 2026 12:40:21 -0500 Subject: [PATCH] fix(client): a new thread's content no longer stays blank after an early 404 A thread-detail fetch that runs before the thread's create command commits gets a 404. The client marked the thread deleted and kept that in the resume cache, so ChatView, mounting after creation, resumed as deleted and never loaded the thread. Only a delete event is now remembered across mounts; an HTTP miss is not. Co-Authored-By: Claude Sonnet 5.5 --- .../src/state/threads-sync.test.ts | 25 +++++++++++++++++++ packages/client-runtime/src/state/threads.ts | 2 ++ 2 files changed, 27 insertions(+) diff --git a/packages/client-runtime/src/state/threads-sync.test.ts b/packages/client-runtime/src/state/threads-sync.test.ts index eaf1751a1c2d2..593e29286b66c 100644 --- a/packages/client-runtime/src/state/threads-sync.test.ts +++ b/packages/client-runtime/src/state/threads-sync.test.ts @@ -673,6 +673,31 @@ describe("EnvironmentThreads", () => { }), ); + it.effect("asks the server again after an HTTP miss, since the thread may not exist yet", () => + Effect.gen(function* () { + const resumeCache: NonNullable[1]> = { + snapshot: undefined, + owner: undefined, + }; + yield* Effect.scoped( + Effect.gen(function* () { + const first = yield* makeHarness({ httpSnapshot: { _tag: "missing" }, resumeCache }); + yield* awaitThreadState(first.observed, (value) => value.status === "deleted"); + }), + ); + const resumed = yield* makeHarness({ + resumeCache, + httpSnapshot: { + _tag: "present", + snapshot: { snapshotSequence: CACHED_SNAPSHOT_SEQUENCE, projection: BASE_PROJECTION }, + }, + }); + const state = yield* awaitThreadState(resumed.observed, (value) => value.status === "live"); + expect(Option.getOrThrow(state.data)).toEqual(BASE_PROJECTION); + expect(yield* Ref.get(resumed.loaderCalls)).toBe(1); + }), + ); + it.effect("resumes a warm cache via afterSequence without an HTTP fetch", () => Effect.gen(function* () { const harness = yield* makeHarness({ cached: BASE_PROJECTION }); diff --git a/packages/client-runtime/src/state/threads.ts b/packages/client-runtime/src/state/threads.ts index c7b22d9dcdff0..5c6622b5ad9ae 100644 --- a/packages/client-runtime/src/state/threads.ts +++ b/packages/client-runtime/src/state/threads.ts @@ -998,6 +998,8 @@ export const makeEnvironmentThreadState = Effect.fn("EnvironmentThreadState.make } case "missing": { yield* setDeleted(); + // A 404 also means "not created yet". Only a delete event may outlive this mount. + if (resumeCache?.owner === owner) resumeCache.snapshot = undefined; return yield* Effect.never; } case "unavailable": {