| author | |
| committer | |
| log | fc262f1a28d8305951c751f2486da6ca72e6c1d1 |
| tree | f16899777f2a1a38b34cf9e4c1e24344f015c168 |
| parent | 053affbed2659f90cd1b1efaaa7a75865c4131c7 |
| signature | Signed by PGP key B5690EEEBB952194 |
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 | ); |
| 680 | 680 | ||
| 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 | ); | ||
| 686 | 699 | ||
| 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 | }); |
| 730 | 744 | ||
| 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"; |
| 22 | import * as FileSystem from "effect/FileSystem"; | 22 | import * as FileSystem from "effect/FileSystem"; |
| 23 | import * as Layer from "effect/Layer"; | 23 | import * as Layer from "effect/Layer"; |
| 24 | import * as Option from "effect/Option"; | 24 | import * as Option from "effect/Option"; |
| 25 | import * as Schedule from "effect/Schedule"; | ||
| 25 | import * as Schema from "effect/Schema"; | 26 | import * as Schema from "effect/Schema"; |
| 26 | import * as Stream from "effect/Stream"; | 27 | import * as Stream from "effect/Stream"; |
| 27 | import { makeDrainableWorker } from "@t3tools/shared/DrainableWorker"; | 28 | import { 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; |
| 907 | 908 | ||
| 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; |
| 915 | 923 | ||
| 916 | const thread = yield* resolveThread(input.threadId); | 924 | const thread = yield* resolveThread(input.threadId); |