Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -728,6 +728,62 @@ describe("ProviderCommandReactor", () => {
expect(thread?.title).toBe("Generated title");
});

effectIt.effect("retries thread title generation after a transient failure", () =>
Effect.gen(function* () {
const harness = yield* Effect.promise(() => createHarness());
const now = "2026-01-01T00:00:00.000Z";
const seededTitle = "Please investigate reconnect failures after restar...";
let attempts = 0;
harness.generateThreadTitle.mockReturnValue(
Effect.suspend(() => {
attempts += 1;
return attempts === 1
? Effect.fail(
new TextGenerationError({
operation: "generateThreadTitle",
detail: "Claude CLI request timed out.",
}),
)
: Effect.succeed({ title: "Generated title" });
}),
);

yield* harness.engine.dispatch({
type: "thread.meta.update",
commandId: CommandId.make("cmd-thread-title-retry-seed"),
threadId: ThreadId.make("thread-1"),
title: seededTitle,
});

yield* harness.engine.dispatch({
type: "thread.turn.start",
commandId: CommandId.make("cmd-turn-start-title-retry"),
threadId: ThreadId.make("thread-1"),
message: {
messageId: asMessageId("user-message-title-retry"),
role: "user",
text: "Please investigate reconnect failures after restarting the session.",
attachments: [],
},
titleSeed: seededTitle,
interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE,
runtimeMode: "approval-required",
createdAt: now,
});

yield* Effect.promise(() =>
waitFor(async () => {
const readModel = await harness.readModel();
return (
readModel.threads.find((entry) => entry.id === ThreadId.make("thread-1"))?.title ===
"Generated title"
);
}),
);
expect(attempts).toBe(2);
}),
);

it("regenerates a thread title from the current conversation", async () => {
const harness = await createHarness();
const now = "2026-01-01T00:00:00.000Z";
Expand Down
26 changes: 20 additions & 6 deletions apps/server/src/orchestration/Layers/ProviderCommandReactor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import * as Equal from "effect/Equal";
import * as FileSystem from "effect/FileSystem";
import * as Layer from "effect/Layer";
import * as Option from "effect/Option";
import * as Schedule from "effect/Schedule";
import * as Schema from "effect/Schema";
import * as Stream from "effect/Stream";
import { makeDrainableWorker } from "@t3tools/shared/DrainableWorker";
Expand Down Expand Up @@ -94,6 +95,12 @@ const HANDLED_TURN_START_KEY_MAX = 10_000;
const HANDLED_TURN_START_KEY_TTL = Duration.minutes(30);
const DEFAULT_RUNTIME_MODE: RuntimeMode = "full-access";
const MAX_REGENERATION_ATTACHMENTS = 4;
// Automatic title generation is fire-and-forget: when it fails the thread keeps
// the placeholder title the client seeded from the first message, and nothing
// ever tries again. Retry so a transient CLI timeout or auth failure does not
// permanently leave the thread named after the raw prompt text.
Comment thread
Bil0000 marked this conversation as resolved.
Outdated
const THREAD_TITLE_GENERATION_RETRIES = 2;
const THREAD_TITLE_RETRY_DELAY = Duration.seconds(2);
const MAX_THREAD_TITLE_CONTEXT_CHARS = 8_000;
const MAX_FIRST_USER_TITLE_CONTEXT_CHARS = 2_000;
const THREAD_TITLE_CONTEXT_TRUNCATION_MARKER = "[Earlier content truncated]\n\n";
Expand Down Expand Up @@ -905,12 +912,19 @@ const make = Effect.gen(function* () {
const { textGenerationModelSelection: modelSelection } =
yield* serverSettingsService.getSettings;

const generated = yield* textGeneration.generateThreadTitle({
cwd: input.cwd,
message: input.messageText,
...(attachments.length > 0 ? { attachments } : {}),
modelSelection,
});
const generated = yield* textGeneration
.generateThreadTitle({
cwd: input.cwd,
message: input.messageText,
...(attachments.length > 0 ? { attachments } : {}),
modelSelection,
})
.pipe(
Effect.retry({
times: THREAD_TITLE_GENERATION_RETRIES,
schedule: Schedule.exponential(THREAD_TITLE_RETRY_DELAY),
}),
);
if (!generated) return;

const thread = yield* resolveThread(input.threadId);
Expand Down
Loading