From aa94a42fddeea4c7343dd3282ad445706b92dde0 Mon Sep 17 00:00:00 2001 From: Theo Browne Date: Tue, 4 Aug 2026 13:38:36 -0700 Subject: [PATCH 1/2] fix(server): generate durable thread titles --- .../src/textGeneration/CodexTextGeneration.ts | 11 +++-- .../TextGenerationPrompts.test.ts | 17 +++++-- .../textGeneration/TextGenerationPrompts.ts | 44 +++++++++++++------ packages/contracts/src/model.ts | 1 + packages/contracts/src/settings.test.ts | 8 ++++ packages/contracts/src/settings.ts | 12 ++++- 6 files changed, 72 insertions(+), 21 deletions(-) diff --git a/apps/server/src/textGeneration/CodexTextGeneration.ts b/apps/server/src/textGeneration/CodexTextGeneration.ts index ad8b9550bc2..0b870ac1d67 100644 --- a/apps/server/src/textGeneration/CodexTextGeneration.ts +++ b/apps/server/src/textGeneration/CodexTextGeneration.ts @@ -7,7 +7,12 @@ import * as Scope from "effect/Scope"; import * as Stream from "effect/Stream"; import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"; -import { type CodexSettings, type ModelSelection } from "@t3tools/contracts"; +import { + type CodexSettings, + DEFAULT_TEXT_GENERATION_REASONING_EFFORT, + type ModelSelection, + TextGenerationError, +} from "@t3tools/contracts"; import { sanitizeBranchFragment, sanitizeFeatureBranchName } from "@t3tools/shared/git"; import { resolveSpawnCommand } from "@t3tools/shared/shell"; @@ -15,7 +20,6 @@ import { resolveAttachmentPath } from "../attachmentStore.ts"; import * as ServerConfig from "../config.ts"; import { expandHomePath } from "../pathExpansion.ts"; import { codexExecLaunchArgs, resolveCodexLaunchArgs } from "../provider/Layers/codexLaunchArgs.ts"; -import { TextGenerationError } from "@t3tools/contracts"; import * as TextGeneration from "./TextGeneration.ts"; import { buildBranchNamePrompt, @@ -33,7 +37,6 @@ import { import { getModelSelectionStringOptionValue } from "@t3tools/shared/model"; import { getCodexServiceTierOptionValue } from "../codexModelOptions.ts"; -const CODEX_GIT_TEXT_GENERATION_REASONING_EFFORT = "low"; const CODEX_TIMEOUT_MS = 180_000; const encodeJsonString = Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown)); /** @@ -178,7 +181,7 @@ export const makeCodexTextGeneration = Effect.fn("makeCodexTextGeneration")(func const launchArgs = resolveCodexLaunchArgs(codexConfig.launchArgs, resolvedEnvironment); const reasoningEffort = getModelSelectionStringOptionValue(modelSelection, "reasoningEffort") ?? - CODEX_GIT_TEXT_GENERATION_REASONING_EFFORT; + DEFAULT_TEXT_GENERATION_REASONING_EFFORT; const serviceTier = getCodexServiceTierOptionValue(modelSelection); const spawnCommand = yield* resolveSpawnCommand( codexConfig.binaryPath || "codex", diff --git a/apps/server/src/textGeneration/TextGenerationPrompts.test.ts b/apps/server/src/textGeneration/TextGenerationPrompts.test.ts index ede18664051..e93be422a3c 100644 --- a/apps/server/src/textGeneration/TextGenerationPrompts.test.ts +++ b/apps/server/src/textGeneration/TextGenerationPrompts.test.ts @@ -154,6 +154,18 @@ describe("buildThreadTitlePrompt", () => { expect(result.prompt).toContain("User message:"); expect(result.prompt).toContain("Investigate reconnect regressions after session restore"); expect(result.prompt).not.toContain("Attachment metadata:"); + expect(result.prompt).toContain( + "Generate a title that will help the user recognize this T3 Code thread weeks later.", + ); + expect(result.prompt).toContain( + "Title the subject and outcome. Discard incidental instructions.", + ); + expect(result.prompt).toContain( + "Name the product change, not the mock, plan, report, branch, or PR used to produce it.", + ); + expect(result.prompt).not.toContain( + "Title should summarize the user's request, not restate it verbatim.", + ); }); it("includes attachment metadata when attachments are provided", () => { @@ -183,12 +195,11 @@ describe("buildThreadTitlePrompt", () => { }); expect(result.prompt).toContain( - "The user requested a new title based on the contents of this thread.", + "Generate a new title that will help the user recognize this T3 Code thread weeks later.", ); expect(result.prompt).toContain('The previous title was "Investigate reconnect regressions".'); - expect(result.prompt).toContain("better represents the current state of the thread"); expect(result.prompt).toContain( - "Capture the thread's intent, not a PR number or other superficial detail.", + "Capture the current durable subject and outcome across the whole thread, not merely its initial request or latest step.", ); expect(result.prompt).toContain("Thread contents:"); expect(result.prompt).toContain("The remaining issue is stale session state"); diff --git a/apps/server/src/textGeneration/TextGenerationPrompts.ts b/apps/server/src/textGeneration/TextGenerationPrompts.ts index 8aed88b1f16..155c9b91427 100644 --- a/apps/server/src/textGeneration/TextGenerationPrompts.ts +++ b/apps/server/src/textGeneration/TextGenerationPrompts.ts @@ -150,6 +150,8 @@ export interface BranchNamePromptInput { interface PromptFromMessageInput { instruction: string; responseShape: string; + guidance?: ReadonlyArray | undefined; + rulesLabel?: string | undefined; rules: ReadonlyArray; message: string; messageLabel?: string | undefined; @@ -177,7 +179,8 @@ function buildPromptFromMessage(input: PromptFromMessageInput): string { const promptSections = [ input.instruction, input.responseShape, - "Rules:", + ...(input.guidance ?? []), + input.rulesLabel ?? "Rules:", ...input.rules.map((rule) => `- ${rule}`), "", `${input.messageLabel ?? "User message"}:`, @@ -234,26 +237,41 @@ export function buildThreadTitlePrompt(input: ThreadTitlePromptInput) { const prompt = buildPromptFromMessage({ instruction: isRegeneration ? [ - "You write concise thread titles for coding conversations.", - "The user requested a new title based on the contents of this thread.", + "Generate a new title that will help the user recognize this T3 Code thread weeks later.", `The previous title was ${JSON.stringify(input.previousTitle)}.`, - "Come up with a new title that better represents the current state of the thread.", ].join("\n") - : "You write concise thread titles for coding conversations.", - responseShape: "Return a JSON object with key: title.", + : "Generate a title that will help the user recognize this T3 Code thread weeks later.", + responseShape: "Return JSON with exactly one key: title.", + guidance: [ + "", + "Before answering, silently reduce the request to:", + "- Subject: What system, feature, or problem is this really about?", + "- Outcome: What does the user ultimately want to understand or change?", + "- Incidental instructions: What only describes how the agent should do the work?", + "", + "Title the subject and outcome. Discard incidental instructions.", + "", + ], + rulesLabel: "Editorial rules:", rules: [ - isRegeneration - ? "Title should summarize the thread's current state, not just its initial request." - : "Title should summarize the user's request, not restate it verbatim.", + "3-8 words, fewer than 40 characters.", + "Use a compact noun phrase or clear action phrase.", + "Capture the umbrella goal when the request lists several symptoms or steps.", + "Name the product change, not the mock, plan, report, branch, or PR used to produce it.", + "Models, subagents, tools, output formats, and monitoring instructions do not belong in the title unless they are themselves the topic.", + '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.', + "For research, name the question domain rather than the requested research process.", + "Do not claim the work is complete.", + "Do not copy and truncate the user's message.", + "Avoid project names already visible in the UI, quotes, labels, filler, and trailing punctuation.", + "Use attached images as primary context for UI issues.", + "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.", ...(isRegeneration ? [ - "Capture the thread's intent, not a PR number or other superficial detail.", + "Capture the current durable subject and outcome across the whole thread, not merely its initial request or latest step.", "Return a different title from the previous title.", ] : []), - "Keep it short and specific (3-8 words).", - "Avoid quotes, filler, prefixes, and trailing punctuation.", - "If images are attached, use them as primary context for visual/UI issues.", ], message: input.message, ...(isRegeneration diff --git a/packages/contracts/src/model.ts b/packages/contracts/src/model.ts index 46067917f13..9fcd0d266dd 100644 --- a/packages/contracts/src/model.ts +++ b/packages/contracts/src/model.ts @@ -145,6 +145,7 @@ export const PREFERRED_DEFAULT_CODEX_MODELS: ReadonlyArray = [ "gpt-5.6-terra", ]; export const DEFAULT_TEXT_GENERATION_MODEL = "gpt-5.6-luna"; +export const DEFAULT_TEXT_GENERATION_REASONING_EFFORT = "low"; export const DEFAULT_MODEL_BY_PROVIDER: Partial> = { [CODEX_DRIVER_KIND]: DEFAULT_MODEL, diff --git a/packages/contracts/src/settings.test.ts b/packages/contracts/src/settings.test.ts index 2bc61d72f21..5bd22e95f20 100644 --- a/packages/contracts/src/settings.test.ts +++ b/packages/contracts/src/settings.test.ts @@ -112,6 +112,14 @@ describe("ClientSettings sidebar v2", () => { }); describe("ServerSettings.providerInstances (slice-2 invariant)", () => { + it("defaults text generation to Luna at low reasoning effort", () => { + expect(DEFAULT_SERVER_SETTINGS.textGenerationModelSelection).toEqual({ + instanceId: ProviderInstanceId.make("codex"), + model: "gpt-5.6-luna", + options: [{ id: "reasoningEffort", value: "low" }], + }); + }); + it("defaults to an empty record so legacy configs without the key still decode", () => { expect(DEFAULT_SERVER_SETTINGS.providerInstances).toEqual({}); }); diff --git a/packages/contracts/src/settings.ts b/packages/contracts/src/settings.ts index 7edda2e52e5..2a0e087d06c 100644 --- a/packages/contracts/src/settings.ts +++ b/packages/contracts/src/settings.ts @@ -3,7 +3,11 @@ import * as Duration from "effect/Duration"; import * as Schema from "effect/Schema"; import * as SchemaTransformation from "effect/SchemaTransformation"; import { TrimmedNonEmptyString, TrimmedString } from "./baseSchemas.ts"; -import { DEFAULT_TEXT_GENERATION_MODEL, ProviderOptionSelections } from "./model.ts"; +import { + DEFAULT_TEXT_GENERATION_MODEL, + DEFAULT_TEXT_GENERATION_REASONING_EFFORT, + ProviderOptionSelections, +} from "./model.ts"; import { ModelSelection } from "./orchestration.ts"; import { ProviderInstanceConfig, ProviderInstanceId } from "./providerInstance.ts"; @@ -498,6 +502,12 @@ export const ServerSettings = Schema.Struct({ Effect.succeed({ instanceId: ProviderInstanceId.make("codex"), model: DEFAULT_TEXT_GENERATION_MODEL, + options: [ + { + id: "reasoningEffort", + value: DEFAULT_TEXT_GENERATION_REASONING_EFFORT, + }, + ], }), ), ), From ed1863e497df2bd5cbd3d08c7513841c50ae4212 Mon Sep 17 00:00:00 2001 From: Theo Browne Date: Tue, 4 Aug 2026 13:58:08 -0700 Subject: [PATCH 2/2] test(server): update Claude title prompt fixture --- apps/server/src/textGeneration/ClaudeTextGeneration.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/server/src/textGeneration/ClaudeTextGeneration.test.ts b/apps/server/src/textGeneration/ClaudeTextGeneration.test.ts index 0f3905a0cb1..e4552eab3e9 100644 --- a/apps/server/src/textGeneration/ClaudeTextGeneration.test.ts +++ b/apps/server/src/textGeneration/ClaudeTextGeneration.test.ts @@ -264,7 +264,7 @@ it.layer(ClaudeTextGenerationTestLayer)("ClaudeTextGeneration", (it) => { ' "Reconnect failures after restart because the session state does not recover" ', }, }), - stdinMustContain: "You write concise thread titles for coding conversations.", + stdinMustContain: "Please investigate reconnect failures after restarting the session.", }, (textGeneration) => Effect.gen(function* () {