authorgravatar for 62337003+Bil0000@users.noreply.github.comBilal Bakr <62337003+Bil0000@users.noreply.github.com> 2026-08-29 18:48:13+03:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-08-29 08:48:13-07:00
logfc262f1a28d8305951c751f2486da6ca72e6c1d1
treef16899777f2a1a38b34cf9e4c1e24344f015c168
parent053affbed2659f90cd1b1efaaa7a75865c4131c7
signature Signed by PGP key B5690EEEBB952194

fix(server): retry automatic thread title generation (#8087)


2 files changed, 30 insertions(+), 8 deletions(-)

apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts+16-2
...@@ -678,11 +678,24 @@ describe("ProviderCommandReactor", () => {...@@ -678,11 +678,24 @@ describe("ProviderCommandReactor", () => {
678 }),678 }),
679 );679 );
680680
681 it("generates a thread title on the first turn", async () => {681 it("retries thread title generation after a transient failure", async () => {
682 const harness = await createHarness();682 const harness = await createHarness();
683 const now = "2026-01-01T00:00:00.000Z";683 const now = "2026-01-01T00:00:00.000Z";
684 const seededTitle = "Please investigate reconnect failures after restar...";684 const seededTitle = "Please investigate reconnect failures after restar...";
685 harness.generateThreadTitle.mockReturnValue(Effect.succeed({ title: "Generated title" }));685 let attempts = 0;
686 harness.generateThreadTitle.mockReturnValue(
687 Effect.suspend(() => {
688 attempts += 1;
689 return attempts === 1
690 ? Effect.fail(
691 new TextGenerationError({
692 operation: "generateThreadTitle",
693 detail: "Claude CLI request timed out.",
694 }),
695 )
696 : Effect.succeed({ title: "Generated title" });
697 }),
698 );
686699
687 await Effect.runPromise(700 await Effect.runPromise(
688 harness.engine.dispatch({701 harness.engine.dispatch({
...@@ -726,6 +739,7 @@ describe("ProviderCommandReactor", () => {...@@ -726,6 +739,7 @@ describe("ProviderCommandReactor", () => {
726 const readModel = await harness.readModel();739 const readModel = await harness.readModel();
727 const thread = readModel.threads.find((entry) => entry.id === ThreadId.make("thread-1"));740 const thread = readModel.threads.find((entry) => entry.id === ThreadId.make("thread-1"));
728 expect(thread?.title).toBe("Generated title");741 expect(thread?.title).toBe("Generated title");
742 expect(attempts).toBe(2);
729 });743 });
730744
731 it("regenerates a thread title from the current conversation", async () => {745 it("regenerates a thread title from the current conversation", async () => {
apps/server/src/orchestration/Layers/ProviderCommandReactor.ts+14-6
...@@ -22,6 +22,7 @@ import * as Equal from "effect/Equal";...@@ -22,6 +22,7 @@ import * as Equal from "effect/Equal";
22import * as FileSystem from "effect/FileSystem";22import * as FileSystem from "effect/FileSystem";
23import * as Layer from "effect/Layer";23import * as Layer from "effect/Layer";
24import * as Option from "effect/Option";24import * as Option from "effect/Option";
25import * as Schedule from "effect/Schedule";
25import * as Schema from "effect/Schema";26import * as Schema from "effect/Schema";
26import * as Stream from "effect/Stream";27import * as Stream from "effect/Stream";
27import { makeDrainableWorker } from "@t3tools/shared/DrainableWorker";28import { makeDrainableWorker } from "@t3tools/shared/DrainableWorker";
...@@ -905,12 +906,19 @@ const make = Effect.gen(function* () {...@@ -905,12 +906,19 @@ const make = Effect.gen(function* () {
905 const { textGenerationModelSelection: modelSelection } =906 const { textGenerationModelSelection: modelSelection } =
906 yield* serverSettingsService.getSettings;907 yield* serverSettingsService.getSettings;
907908
908 const generated = yield* textGeneration.generateThreadTitle({909 const generated = yield* textGeneration
909 cwd: input.cwd,910 .generateThreadTitle({
910 message: input.messageText,911 cwd: input.cwd,
911 ...(attachments.length > 0 ? { attachments } : {}),912 message: input.messageText,
912 modelSelection,913 ...(attachments.length > 0 ? { attachments } : {}),
913 });914 modelSelection,
915 })
916 .pipe(
917 Effect.retry({
918 times: 2,
919 schedule: Schedule.exponential("2 seconds"),
920 }),
921 );
914 if (!generated) return;922 if (!generated) return;
915923
916 const thread = yield* resolveThread(input.threadId);924 const thread = yield* resolveThread(input.threadId);