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", () => {
678678 }),
679679 );
680680
681 it("generates a thread title on the first turn", async () => {
681 it("retries thread title generation after a transient failure", async () => {
682682 const harness = await createHarness();
683683 const now = "2026-01-01T00:00:00.000Z";
684684 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
687700 await Effect.runPromise(
688701 harness.engine.dispatch({
......@@ -726,6 +739,7 @@ describe("ProviderCommandReactor", () => {
726739 const readModel = await harness.readModel();
727740 const thread = readModel.threads.find((entry) => entry.id === ThreadId.make("thread-1"));
728741 expect(thread?.title).toBe("Generated title");
742 expect(attempts).toBe(2);
729743 });
730744
731745 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";
2222import * as FileSystem from "effect/FileSystem";
2323import * as Layer from "effect/Layer";
2424import * as Option from "effect/Option";
25import * as Schedule from "effect/Schedule";
2526import * as Schema from "effect/Schema";
2627import * as Stream from "effect/Stream";
2728import { makeDrainableWorker } from "@t3tools/shared/DrainableWorker";
......@@ -905,12 +906,19 @@ const make = Effect.gen(function* () {
905906 const { textGenerationModelSelection: modelSelection } =
906907 yield* serverSettingsService.getSettings;
907908
908 const generated = yield* textGeneration.generateThreadTitle({
909 cwd: input.cwd,
910 message: input.messageText,
911 ...(attachments.length > 0 ? { attachments } : {}),
912 modelSelection,
913 });
909 const generated = yield* textGeneration
910 .generateThreadTitle({
911 cwd: input.cwd,
912 message: input.messageText,
913 ...(attachments.length > 0 ? { attachments } : {}),
914 modelSelection,
915 })
916 .pipe(
917 Effect.retry({
918 times: 2,
919 schedule: Schedule.exponential("2 seconds"),
920 }),
921 );
914922 if (!generated) return;
915923
916924 const thread = yield* resolveThread(input.threadId);