authorgravatar for 67719167+Chrrxs@users.noreply.github.comChris Michael Guzman <67719167+Chrrxs@users.noreply.github.com> 2026-07-18 07:16:38-04:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-07-18 13:16:38+02:00
log765e1b5fc044b899741a11dcba141bb2c49c6c37
tree372b1f3cf9c72702e515b089f410eed1943bf86d
parent03e20c875ccd39e4cedba8f6b713195290ec4652
signature Signed by PGP key B5690EEEBB952194

perf(client): defer active thread cache writes (#4006)


2 files changed, 60 insertions(+), 5 deletions(-)

packages/client-runtime/src/state/threads-sync.test.ts+46
......@@ -5,6 +5,7 @@ import {
55 ProjectId,
66 ProviderInstanceId,
77 ThreadId,
8 TurnId,
89 type OrchestrationThread,
910 type OrchestrationThreadDetailSnapshot,
1011 type OrchestrationThreadStreamItem,
......@@ -74,6 +75,26 @@ const BASE_THREAD: OrchestrationThread = {
7475 checkpoints: [],
7576 session: null,
7677};
78const ACTIVE_THREAD: OrchestrationThread = {
79 ...BASE_THREAD,
80 latestTurn: {
81 turnId: TurnId.make("turn-1"),
82 state: "running",
83 requestedAt: "2026-04-01T00:01:00.000Z",
84 startedAt: "2026-04-01T00:01:00.000Z",
85 completedAt: null,
86 assistantMessageId: null,
87 },
88 session: {
89 threadId: THREAD_ID,
90 status: "running",
91 providerName: "codex",
92 runtimeMode: "full-access",
93 activeTurnId: TurnId.make("turn-1"),
94 lastError: null,
95 updatedAt: "2026-04-01T00:01:00.000Z",
96 },
97};
7798
7899type TestThreadInput = OrchestrationThreadStreamItem | Error;
79100
......@@ -308,6 +329,31 @@ describe("EnvironmentThreads", () => {
308329 }),
309330 );
310331
332 it.effect("does not persist active thread snapshots during streaming or teardown", () =>
333 Effect.gen(function* () {
334 const savedThreads = yield* Effect.scoped(
335 Effect.gen(function* () {
336 const harness = yield* makeHarness({ cached: ACTIVE_THREAD });
337 yield* awaitThreadState(
338 harness.observed,
339 (value) =>
340 value.status === "live" &&
341 Option.isSome(value.data) &&
342 value.data.value.session?.status === "running",
343 );
344
345 yield* TestClock.adjust("500 millis");
346 yield* Effect.yieldNow;
347
348 expect(yield* Ref.get(harness.savedThreads)).toEqual([]);
349 return harness.savedThreads;
350 }),
351 );
352
353 expect(yield* Ref.get(savedThreads)).toEqual([]);
354 }),
355 );
356
311357 it.effect("seeds the thread from the HTTP snapshot and resumes live events", () =>
312358 Effect.gen(function* () {
313359 const httpThread: OrchestrationThread = { ...BASE_THREAD, title: "HTTP title" };
packages/client-runtime/src/state/threads.ts+14-5
......@@ -41,6 +41,11 @@ function formatThreadError(cause: Cause.Cause<unknown>): string {
4141 : "Could not synchronize the thread.";
4242}
4343
44function shouldPersistThread(thread: OrchestrationThread): boolean {
45 const status = thread.session?.status;
46 return status !== "starting" && status !== "running";
47}
48
4449export const makeEnvironmentThreadState = Effect.fn("EnvironmentThreadState.make")(function* (
4550 threadId: ThreadIdType,
4651) {
......@@ -128,10 +133,13 @@ export const makeEnvironmentThreadState = Effect.fn("EnvironmentThreadState.make
128133 status: "live",
129134 error: Option.none(),
130135 });
131 // Persist the thread together with the sequence it reflects so the next warm
132 // cache can resume from exactly here.
133 const snapshotSequence = yield* SubscriptionRef.get(lastSequence);
134 yield* Queue.offer(persistence, { snapshotSequence, thread });
136 // Active threads can update many times per second and retain large tool
137 // payloads. The server remains the source of truth while a turn is active;
138 // persist once it settles so cache encoding stays off the streaming path.
139 if (shouldPersistThread(thread)) {
140 const snapshotSequence = yield* SubscriptionRef.get(lastSequence);
141 yield* Queue.offer(persistence, { snapshotSequence, thread });
142 }
135143 });
136144
137145 const setDeleted = Effect.fn("EnvironmentThreadState.setDeleted")(function* () {
......@@ -246,7 +254,8 @@ export const makeEnvironmentThreadState = Effect.fn("EnvironmentThreadState.make
246254 Effect.flatMap(([current, snapshotSequence]) =>
247255 Option.match(current.data, {
248256 onNone: () => Effect.void,
249 onSome: (thread) => persist({ snapshotSequence, thread }),
257 onSome: (thread) =>
258 shouldPersistThread(thread) ? persist({ snapshotSequence, thread }) : Effect.void,
250259 }),
251260 ),
252261 ),