authorgravatar for me@t3.ggTheo Browne <me@t3.gg> 2026-08-04 14:02:33-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-08-04 14:02:33-07:00
logd7950ac153c6fdd788ef63699a5d061243bb4997
tree353f22b5b75a2199d4fdeef321ca8886d163c459
parent36caf34c68b7bc6c2e967fdec1812fd08f61cdc8
signature Signed by PGP key B5690EEEBB952194

fix(server): generate durable thread titles (#5357)


7 files changed, 73 insertions(+), 22 deletions(-)

apps/server/src/textGeneration/ClaudeTextGeneration.test.ts+1-1
......@@ -264,7 +264,7 @@ it.layer(ClaudeTextGenerationTestLayer)("ClaudeTextGeneration", (it) => {
264264 ' "Reconnect failures after restart because the session state does not recover" ',
265265 },
266266 }),
267 stdinMustContain: "You write concise thread titles for coding conversations.",
267 stdinMustContain: "Please investigate reconnect failures after restarting the session.",
268268 },
269269 (textGeneration) =>
270270 Effect.gen(function* () {
apps/server/src/textGeneration/CodexTextGeneration.ts+7-4
......@@ -7,7 +7,12 @@ import * as Scope from "effect/Scope";
77import * as Stream from "effect/Stream";
88import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
99
10import { type CodexSettings, type ModelSelection } from "@t3tools/contracts";
10import {
11 type CodexSettings,
12 DEFAULT_TEXT_GENERATION_REASONING_EFFORT,
13 type ModelSelection,
14 TextGenerationError,
15} from "@t3tools/contracts";
1116import { sanitizeBranchFragment, sanitizeFeatureBranchName } from "@t3tools/shared/git";
1217import { resolveSpawnCommand } from "@t3tools/shared/shell";
1318
......@@ -15,7 +20,6 @@ import { resolveAttachmentPath } from "../attachmentStore.ts";
1520import * as ServerConfig from "../config.ts";
1621import { expandHomePath } from "../pathExpansion.ts";
1722import { codexExecLaunchArgs, resolveCodexLaunchArgs } from "../provider/Layers/codexLaunchArgs.ts";
18import { TextGenerationError } from "@t3tools/contracts";
1923import * as TextGeneration from "./TextGeneration.ts";
2024import {
2125 buildBranchNamePrompt,
......@@ -33,7 +37,6 @@ import {
3337import { getModelSelectionStringOptionValue } from "@t3tools/shared/model";
3438import { getCodexServiceTierOptionValue } from "../codexModelOptions.ts";
3539
36const CODEX_GIT_TEXT_GENERATION_REASONING_EFFORT = "low";
3740const CODEX_TIMEOUT_MS = 180_000;
3841const encodeJsonString = Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown));
3942/**
......@@ -178,7 +181,7 @@ export const makeCodexTextGeneration = Effect.fn("makeCodexTextGeneration")(func
178181 const launchArgs = resolveCodexLaunchArgs(codexConfig.launchArgs, resolvedEnvironment);
179182 const reasoningEffort =
180183 getModelSelectionStringOptionValue(modelSelection, "reasoningEffort") ??
181 CODEX_GIT_TEXT_GENERATION_REASONING_EFFORT;
184 DEFAULT_TEXT_GENERATION_REASONING_EFFORT;
182185 const serviceTier = getCodexServiceTierOptionValue(modelSelection);
183186 const spawnCommand = yield* resolveSpawnCommand(
184187 codexConfig.binaryPath || "codex",
apps/server/src/textGeneration/TextGenerationPrompts.test.ts+14-3
......@@ -154,6 +154,18 @@ describe("buildThreadTitlePrompt", () => {
154154 expect(result.prompt).toContain("User message:");
155155 expect(result.prompt).toContain("Investigate reconnect regressions after session restore");
156156 expect(result.prompt).not.toContain("Attachment metadata:");
157 expect(result.prompt).toContain(
158 "Generate a title that will help the user recognize this T3 Code thread weeks later.",
159 );
160 expect(result.prompt).toContain(
161 "Title the subject and outcome. Discard incidental instructions.",
162 );
163 expect(result.prompt).toContain(
164 "Name the product change, not the mock, plan, report, branch, or PR used to produce it.",
165 );
166 expect(result.prompt).not.toContain(
167 "Title should summarize the user's request, not restate it verbatim.",
168 );
157169 });
158170
159171 it("includes attachment metadata when attachments are provided", () => {
......@@ -183,12 +195,11 @@ describe("buildThreadTitlePrompt", () => {
183195 });
184196
185197 expect(result.prompt).toContain(
186 "The user requested a new title based on the contents of this thread.",
198 "Generate a new title that will help the user recognize this T3 Code thread weeks later.",
187199 );
188200 expect(result.prompt).toContain('The previous title was "Investigate reconnect regressions".');
189 expect(result.prompt).toContain("better represents the current state of the thread");
190201 expect(result.prompt).toContain(
191 "Capture the thread's intent, not a PR number or other superficial detail.",
202 "Capture the current durable subject and outcome across the whole thread, not merely its initial request or latest step.",
192203 );
193204 expect(result.prompt).toContain("Thread contents:");
194205 expect(result.prompt).toContain("The remaining issue is stale session state");
apps/server/src/textGeneration/TextGenerationPrompts.ts+31-13
......@@ -150,6 +150,8 @@ export interface BranchNamePromptInput {
150150interface PromptFromMessageInput {
151151 instruction: string;
152152 responseShape: string;
153 guidance?: ReadonlyArray<string> | undefined;
154 rulesLabel?: string | undefined;
153155 rules: ReadonlyArray<string>;
154156 message: string;
155157 messageLabel?: string | undefined;
......@@ -177,7 +179,8 @@ function buildPromptFromMessage(input: PromptFromMessageInput): string {
177179 const promptSections = [
178180 input.instruction,
179181 input.responseShape,
180 "Rules:",
182 ...(input.guidance ?? []),
183 input.rulesLabel ?? "Rules:",
181184 ...input.rules.map((rule) => `- ${rule}`),
182185 "",
183186 `${input.messageLabel ?? "User message"}:`,
......@@ -234,26 +237,41 @@ export function buildThreadTitlePrompt(input: ThreadTitlePromptInput) {
234237 const prompt = buildPromptFromMessage({
235238 instruction: isRegeneration
236239 ? [
237 "You write concise thread titles for coding conversations.",
238 "The user requested a new title based on the contents of this thread.",
240 "Generate a new title that will help the user recognize this T3 Code thread weeks later.",
239241 `The previous title was ${JSON.stringify(input.previousTitle)}.`,
240 "Come up with a new title that better represents the current state of the thread.",
241242 ].join("\n")
242 : "You write concise thread titles for coding conversations.",
243 responseShape: "Return a JSON object with key: title.",
243 : "Generate a title that will help the user recognize this T3 Code thread weeks later.",
244 responseShape: "Return JSON with exactly one key: title.",
245 guidance: [
246 "",
247 "Before answering, silently reduce the request to:",
248 "- Subject: What system, feature, or problem is this really about?",
249 "- Outcome: What does the user ultimately want to understand or change?",
250 "- Incidental instructions: What only describes how the agent should do the work?",
251 "",
252 "Title the subject and outcome. Discard incidental instructions.",
253 "",
254 ],
255 rulesLabel: "Editorial rules:",
244256 rules: [
245 isRegeneration
246 ? "Title should summarize the thread's current state, not just its initial request."
247 : "Title should summarize the user's request, not restate it verbatim.",
257 "3-8 words, fewer than 40 characters.",
258 "Use a compact noun phrase or clear action phrase.",
259 "Capture the umbrella goal when the request lists several symptoms or steps.",
260 "Name the product change, not the mock, plan, report, branch, or PR used to produce it.",
261 "Models, subagents, tools, output formats, and monitoring instructions do not belong in the title unless they are themselves the topic.",
262 'For reviews, name what is being reviewed and the relevant concern. Avoid generic titles such as "Review PR 123" when linked or attached context reveals the subject.',
263 "For research, name the question domain rather than the requested research process.",
264 "Do not claim the work is complete.",
265 "Do not copy and truncate the user's message.",
266 "Avoid project names already visible in the UI, quotes, labels, filler, and trailing punctuation.",
267 "Use attached images as primary context for UI issues.",
268 "When a URL or attachment is the only source of the subject, use available tools to inspect it. If it cannot be resolved, remain accurate rather than guessing.",
248269 ...(isRegeneration
249270 ? [
250 "Capture the thread's intent, not a PR number or other superficial detail.",
271 "Capture the current durable subject and outcome across the whole thread, not merely its initial request or latest step.",
251272 "Return a different title from the previous title.",
252273 ]
253274 : []),
254 "Keep it short and specific (3-8 words).",
255 "Avoid quotes, filler, prefixes, and trailing punctuation.",
256 "If images are attached, use them as primary context for visual/UI issues.",
257275 ],
258276 message: input.message,
259277 ...(isRegeneration
packages/contracts/src/model.ts+1
......@@ -145,6 +145,7 @@ export const PREFERRED_DEFAULT_CODEX_MODELS: ReadonlyArray<string> = [
145145 "gpt-5.6-terra",
146146];
147147export const DEFAULT_TEXT_GENERATION_MODEL = "gpt-5.6-luna";
148export const DEFAULT_TEXT_GENERATION_REASONING_EFFORT = "low";
148149
149150export const DEFAULT_MODEL_BY_PROVIDER: Partial<Record<ProviderDriverKind, string>> = {
150151 [CODEX_DRIVER_KIND]: DEFAULT_MODEL,
packages/contracts/src/settings.test.ts+8
......@@ -112,6 +112,14 @@ describe("ClientSettings sidebar v2", () => {
112112});
113113
114114describe("ServerSettings.providerInstances (slice-2 invariant)", () => {
115 it("defaults text generation to Luna at low reasoning effort", () => {
116 expect(DEFAULT_SERVER_SETTINGS.textGenerationModelSelection).toEqual({
117 instanceId: ProviderInstanceId.make("codex"),
118 model: "gpt-5.6-luna",
119 options: [{ id: "reasoningEffort", value: "low" }],
120 });
121 });
122
115123 it("defaults to an empty record so legacy configs without the key still decode", () => {
116124 expect(DEFAULT_SERVER_SETTINGS.providerInstances).toEqual({});
117125 });
packages/contracts/src/settings.ts+11-1
......@@ -3,7 +3,11 @@ import * as Duration from "effect/Duration";
33import * as Schema from "effect/Schema";
44import * as SchemaTransformation from "effect/SchemaTransformation";
55import { TrimmedNonEmptyString, TrimmedString } from "./baseSchemas.ts";
6import { DEFAULT_TEXT_GENERATION_MODEL, ProviderOptionSelections } from "./model.ts";
6import {
7 DEFAULT_TEXT_GENERATION_MODEL,
8 DEFAULT_TEXT_GENERATION_REASONING_EFFORT,
9 ProviderOptionSelections,
10} from "./model.ts";
711import { ModelSelection } from "./orchestration.ts";
812import { ProviderInstanceConfig, ProviderInstanceId } from "./providerInstance.ts";
913
......@@ -498,6 +502,12 @@ export const ServerSettings = Schema.Struct({
498502 Effect.succeed({
499503 instanceId: ProviderInstanceId.make("codex"),
500504 model: DEFAULT_TEXT_GENERATION_MODEL,
505 options: [
506 {
507 id: "reasoningEffort",
508 value: DEFAULT_TEXT_GENERATION_REASONING_EFFORT,
509 },
510 ],
501511 }),
502512 ),
503513 ),