| author | |
| committer | |
| log | 9bd2a4c6886a4c20e0a0a937dc98dd118c645c8f |
| tree | a042d1db723b3d7513ac915881fa6e9eead147e2 |
| parent | 4fb03aff0bcddf3379e9f9277a87e8eae994a2ae |
| signature | Signed by PGP key B5690EEEBB952194 |
4 files changed, 290 insertions(+), 60 deletions(-)
apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts+130-4| ... | ... | @@ -785,6 +785,123 @@ describe("ProviderCommandReactor", () => { |
| 785 | 785 | expect(thread?.titleRegeneration).toBeNull(); |
| 786 | 786 | }); |
| 787 | 787 | |
| 788 | it("pins the first user message when regeneration context is truncated", async () => { | |
| 789 | const harness = await createHarness(); | |
| 790 | const now = "2026-01-01T00:00:00.000Z"; | |
| 791 | const firstUserMessage = `Review subagent monitoring risks. ${"Opening context. ".repeat(200)}`; | |
| 792 | const recentUserMessage = `LATEST FINDING: ${"implementation detail ".repeat(320)}`; | |
| 793 | harness.generateThreadTitle.mockReturnValue( | |
| 794 | Effect.succeed({ title: "Review subagent monitoring risks" }), | |
| 795 | ); | |
| 796 | ||
| 797 | await harness.runEffect( | |
| 798 | harness.engine.dispatch({ | |
| 799 | type: "thread.meta.update", | |
| 800 | commandId: CommandId.make("cmd-thread-title-existing-long"), | |
| 801 | threadId: ThreadId.make("thread-1"), | |
| 802 | title: "Generic PR review", | |
| 803 | }), | |
| 804 | ); | |
| 805 | await harness.runEffect( | |
| 806 | harness.engine.dispatch({ | |
| 807 | type: "thread.turn.start", | |
| 808 | commandId: CommandId.make("cmd-turn-start-before-long-title-regeneration"), | |
| 809 | threadId: ThreadId.make("thread-1"), | |
| 810 | message: { | |
| 811 | messageId: asMessageId("user-message-before-long-title-regeneration"), | |
| 812 | role: "user", | |
| 813 | text: firstUserMessage, | |
| 814 | attachments: [ | |
| 815 | { | |
| 816 | type: "image", | |
| 817 | id: "opening-context-image", | |
| 818 | name: "image.png", | |
| 819 | mimeType: "image/png", | |
| 820 | sizeBytes: 5, | |
| 821 | }, | |
| 822 | ], | |
| 823 | }, | |
| 824 | interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, | |
| 825 | runtimeMode: "approval-required", | |
| 826 | createdAt: now, | |
| 827 | }), | |
| 828 | ); | |
| 829 | await harness.runEffect( | |
| 830 | harness.engine.dispatch({ | |
| 831 | type: "thread.turn.start", | |
| 832 | commandId: CommandId.make("cmd-middle-turn-before-long-title-regeneration"), | |
| 833 | threadId: ThreadId.make("thread-1"), | |
| 834 | message: { | |
| 835 | messageId: asMessageId("middle-message-before-long-title-regeneration"), | |
| 836 | role: "user", | |
| 837 | text: "Temporary handoff details.", | |
| 838 | attachments: [ | |
| 839 | { | |
| 840 | type: "image", | |
| 841 | id: "middle-context-image", | |
| 842 | name: "image.png", | |
| 843 | mimeType: "image/png", | |
| 844 | sizeBytes: 5, | |
| 845 | }, | |
| 846 | ], | |
| 847 | }, | |
| 848 | interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, | |
| 849 | runtimeMode: "approval-required", | |
| 850 | createdAt: "2026-01-01T00:00:01.000Z", | |
| 851 | }), | |
| 852 | ); | |
| 853 | await harness.runEffect( | |
| 854 | harness.engine.dispatch({ | |
| 855 | type: "thread.turn.start", | |
| 856 | commandId: CommandId.make("cmd-recent-turn-before-long-title-regeneration"), | |
| 857 | threadId: ThreadId.make("thread-1"), | |
| 858 | message: { | |
| 859 | messageId: asMessageId("recent-message-before-long-title-regeneration"), | |
| 860 | role: "user", | |
| 861 | text: recentUserMessage, | |
| 862 | attachments: [ | |
| 863 | { | |
| 864 | type: "image", | |
| 865 | id: "recent-context-image", | |
| 866 | name: "image.png", | |
| 867 | mimeType: "image/png", | |
| 868 | sizeBytes: 5, | |
| 869 | }, | |
| 870 | ], | |
| 871 | }, | |
| 872 | interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, | |
| 873 | runtimeMode: "approval-required", | |
| 874 | createdAt: "2026-01-01T00:00:02.000Z", | |
| 875 | }), | |
| 876 | ); | |
| 877 | await harness.runEffect( | |
| 878 | harness.engine.dispatch({ | |
| 879 | type: "thread.meta.update", | |
| 880 | commandId: CommandId.make("cmd-thread-title-regenerate-long"), | |
| 881 | threadId: ThreadId.make("thread-1"), | |
| 882 | regenerateTitle: true, | |
| 883 | }), | |
| 884 | ); | |
| 885 | ||
| 886 | await harness.drain(); | |
| 887 | ||
| 888 | expect(harness.generateThreadTitle).toHaveBeenCalledTimes(1); | |
| 889 | const input = harness.generateThreadTitle.mock.calls[0]?.[0]; | |
| 890 | if (!input) { | |
| 891 | throw new Error("Expected a title generation input"); | |
| 892 | } | |
| 893 | const message = input.message; | |
| 894 | expect(message.startsWith("USER:\nReview subagent monitoring risks.")).toBe(true); | |
| 895 | expect(message).toContain("[First user message truncated]"); | |
| 896 | expect(message).toContain("[Earlier content truncated]"); | |
| 897 | expect(message).toContain("image.png"); | |
| 898 | expect(message).toHaveLength(8_000); | |
| 899 | expect(input.attachments?.map((attachment) => attachment.id)).toEqual([ | |
| 900 | "opening-context-image", | |
| 901 | "recent-context-image", | |
| 902 | ]); | |
| 903 | }); | |
| 904 | ||
| 788 | 905 | it("clears title regeneration state left pending across reactor startup", async () => { |
| 789 | 906 | const harness = await createHarness({ |
| 790 | 907 | titleRegenerationBeforeStart: "one", |
| ... | ... | @@ -972,10 +1089,14 @@ describe("ProviderCommandReactor", () => { |
| 972 | 1089 | expect(thread?.titleRegeneration).toBeNull(); |
| 973 | 1090 | }); |
| 974 | 1091 | |
| 975 | it("keeps the full retained context and excludes attachments outside it", async () => { | |
| 1092 | it("pins the first user context and attachment before the retained tail", async () => { | |
| 976 | 1093 | const harness = await createHarness(); |
| 977 | 1094 | const now = "2026-01-01T00:00:00.000Z"; |
| 978 | const retainedContext = "x".repeat(8_000); | |
| 1095 | const firstUserContext = "USER:\nOld visual issue\n[Attachments: old-issue.png]"; | |
| 1096 | const truncationMarker = "[Earlier content truncated]\n\n"; | |
| 1097 | const retainedContext = "x".repeat( | |
| 1098 | 8_000 - firstUserContext.length - "\n\n".length - truncationMarker.length, | |
| 1099 | ); | |
| 979 | 1100 | |
| 980 | 1101 | await harness.runEffect( |
| 981 | 1102 | harness.engine.dispatch({ |
| ... | ... | @@ -1040,9 +1161,14 @@ describe("ProviderCommandReactor", () => { |
| 1040 | 1161 | await harness.drain(); |
| 1041 | 1162 | |
| 1042 | 1163 | expect(harness.generateThreadTitle.mock.calls[0]?.[0].message).toBe( |
| 1043 | `[Earlier content truncated]\n\n${retainedContext}`, | |
| 1164 | `${firstUserContext}\n\n${truncationMarker}${retainedContext}`, | |
| 1044 | 1165 | ); |
| 1045 | expect(harness.generateThreadTitle.mock.calls[0]?.[0].attachments).toBeUndefined(); | |
| 1166 | expect(harness.generateThreadTitle.mock.calls[0]?.[0].attachments).toEqual([ | |
| 1167 | expect.objectContaining({ | |
| 1168 | id: "old-title-context-image", | |
| 1169 | name: "old-issue.png", | |
| 1170 | }), | |
| 1171 | ]); | |
| 1046 | 1172 | }); |
| 1047 | 1173 | |
| 1048 | 1174 | it("does not overwrite a manual rename while title regeneration is running", async () => { |
apps/server/src/orchestration/Layers/ProviderCommandReactor.ts+88-23| ... | ... | @@ -94,41 +94,61 @@ const DEFAULT_RUNTIME_MODE: RuntimeMode = "full-access"; |
| 94 | 94 | const DEFAULT_THREAD_TITLE = "New thread"; |
| 95 | 95 | const MAX_REGENERATION_ATTACHMENTS = 4; |
| 96 | 96 | const MAX_THREAD_TITLE_CONTEXT_CHARS = 8_000; |
| 97 | const MAX_FIRST_USER_TITLE_CONTEXT_CHARS = 2_000; | |
| 97 | 98 | const THREAD_TITLE_CONTEXT_TRUNCATION_MARKER = "[Earlier content truncated]\n\n"; |
| 99 | const FIRST_USER_CONTEXT_TRUNCATION_MARKER = "\n[First user message truncated]"; | |
| 98 | 100 | |
| 99 | function formatThreadTitleContext( | |
| 100 | messages: ReadonlyArray<{ | |
| 101 | readonly role: "user" | "assistant" | "system"; | |
| 102 | readonly text: string; | |
| 103 | readonly attachments?: ReadonlyArray<ChatAttachment> | undefined; | |
| 104 | }>, | |
| 101 | type ThreadTitleMessage = { | |
| 102 | readonly role: "user" | "assistant" | "system"; | |
| 103 | readonly text: string; | |
| 104 | readonly attachments?: ReadonlyArray<ChatAttachment> | undefined; | |
| 105 | }; | |
| 106 | ||
| 107 | function formatThreadTitleSection(message: ThreadTitleMessage): string | undefined { | |
| 108 | if (message.role === "system") { | |
| 109 | return undefined; | |
| 110 | } | |
| 111 | const text = message.text.trim(); | |
| 112 | const attachmentSummary = (message.attachments ?? []) | |
| 113 | .map((attachment) => attachment.name) | |
| 114 | .join(", "); | |
| 115 | const contents = [ | |
| 116 | ...(text.length > 0 ? [text] : []), | |
| 117 | ...(attachmentSummary.length > 0 ? [`[Attachments: ${attachmentSummary}]`] : []), | |
| 118 | ].join("\n"); | |
| 119 | return contents.length > 0 ? `${message.role.toUpperCase()}:\n${contents}` : undefined; | |
| 120 | } | |
| 121 | ||
| 122 | function limitFirstUserSection(section: string): string { | |
| 123 | if (section.length <= MAX_FIRST_USER_TITLE_CONTEXT_CHARS) { | |
| 124 | return section; | |
| 125 | } | |
| 126 | return `${section.slice( | |
| 127 | 0, | |
| 128 | MAX_FIRST_USER_TITLE_CONTEXT_CHARS - FIRST_USER_CONTEXT_TRUNCATION_MARKER.length, | |
| 129 | )}${FIRST_USER_CONTEXT_TRUNCATION_MARKER}`; | |
| 130 | } | |
| 131 | ||
| 132 | function collectRecentThreadTitleContext( | |
| 133 | messages: ReadonlyArray<ThreadTitleMessage>, | |
| 134 | maxChars: number, | |
| 105 | 135 | ): { |
| 106 | readonly message: string; | |
| 136 | readonly context: string; | |
| 107 | 137 | readonly attachments: ReadonlyArray<ChatAttachment>; |
| 138 | readonly truncated: boolean; | |
| 108 | 139 | } { |
| 109 | 140 | let context = ""; |
| 110 | 141 | let truncated = false; |
| 111 | 142 | const retainedAttachments: Array<ChatAttachment> = []; |
| 112 | 143 | |
| 113 | 144 | for (const message of messages.toReversed()) { |
| 114 | if (message.role === "system") { | |
| 115 | continue; | |
| 116 | } | |
| 117 | const text = message.text.trim(); | |
| 118 | const attachmentSummary = (message.attachments ?? []) | |
| 119 | .map((attachment) => attachment.name) | |
| 120 | .join(", "); | |
| 121 | const contents = [ | |
| 122 | ...(text.length > 0 ? [text] : []), | |
| 123 | ...(attachmentSummary.length > 0 ? [`[Attachments: ${attachmentSummary}]`] : []), | |
| 124 | ].join("\n"); | |
| 125 | if (contents.length === 0) { | |
| 145 | const section = formatThreadTitleSection(message); | |
| 146 | if (section === undefined) { | |
| 126 | 147 | continue; |
| 127 | 148 | } |
| 128 | 149 | |
| 129 | const section = `${message.role.toUpperCase()}:\n${contents}`; | |
| 130 | 150 | const separator = context.length > 0 ? "\n\n" : ""; |
| 131 | const available = MAX_THREAD_TITLE_CONTEXT_CHARS - context.length - separator.length; | |
| 151 | const available = maxChars - context.length - separator.length; | |
| 132 | 152 | if (section.length > available) { |
| 133 | 153 | if (available > 0) { |
| 134 | 154 | context = `${section.slice(-available)}${separator}${context}`; |
| ... | ... | @@ -141,9 +161,54 @@ function formatThreadTitleContext( |
| 141 | 161 | retainedAttachments.unshift(...(message.attachments ?? [])); |
| 142 | 162 | } |
| 143 | 163 | |
| 164 | return { context, attachments: retainedAttachments, truncated }; | |
| 165 | } | |
| 166 | ||
| 167 | function formatThreadTitleContext(messages: ReadonlyArray<ThreadTitleMessage>): { | |
| 168 | readonly message: string; | |
| 169 | readonly attachments: ReadonlyArray<ChatAttachment>; | |
| 170 | } { | |
| 171 | const recent = collectRecentThreadTitleContext(messages, MAX_THREAD_TITLE_CONTEXT_CHARS); | |
| 172 | if (!recent.truncated) { | |
| 173 | return { | |
| 174 | message: recent.context, | |
| 175 | attachments: recent.attachments.slice(-MAX_REGENERATION_ATTACHMENTS), | |
| 176 | }; | |
| 177 | } | |
| 178 | ||
| 179 | const firstUserMessage = messages.find( | |
| 180 | (message) => message.role === "user" && formatThreadTitleSection(message), | |
| 181 | ); | |
| 182 | const firstUserSection = firstUserMessage | |
| 183 | ? formatThreadTitleSection(firstUserMessage) | |
| 184 | : undefined; | |
| 185 | if (!firstUserMessage || !firstUserSection) { | |
| 186 | return { | |
| 187 | message: `${THREAD_TITLE_CONTEXT_TRUNCATION_MARKER}${recent.context}`, | |
| 188 | attachments: recent.attachments.slice(-MAX_REGENERATION_ATTACHMENTS), | |
| 189 | }; | |
| 190 | } | |
| 191 | ||
| 192 | const pinnedSection = limitFirstUserSection(firstUserSection); | |
| 193 | const recentContextBudget = | |
| 194 | MAX_THREAD_TITLE_CONTEXT_CHARS - | |
| 195 | pinnedSection.length - | |
| 196 | "\n\n".length - | |
| 197 | THREAD_TITLE_CONTEXT_TRUNCATION_MARKER.length; | |
| 198 | const retainedRecent = collectRecentThreadTitleContext(messages, recentContextBudget); | |
| 199 | const pinnedAttachment = firstUserMessage.attachments?.[0]; | |
| 200 | const recentAttachments = retainedRecent.attachments.filter( | |
| 201 | (attachment) => attachment.id !== pinnedAttachment?.id, | |
| 202 | ); | |
| 203 | ||
| 144 | 204 | return { |
| 145 | message: truncated ? `${THREAD_TITLE_CONTEXT_TRUNCATION_MARKER}${context}` : context, | |
| 146 | attachments: retainedAttachments.slice(-MAX_REGENERATION_ATTACHMENTS), | |
| 205 | message: `${pinnedSection}\n\n${THREAD_TITLE_CONTEXT_TRUNCATION_MARKER}${retainedRecent.context}`, | |
| 206 | attachments: [ | |
| 207 | ...(pinnedAttachment ? [pinnedAttachment] : []), | |
| 208 | ...recentAttachments.slice( | |
| 209 | -(MAX_REGENERATION_ATTACHMENTS - (pinnedAttachment === undefined ? 0 : 1)), | |
| 210 | ), | |
| 211 | ], | |
| 147 | 212 | }; |
| 148 | 213 | } |
| 149 | 214 |
apps/server/src/textGeneration/TextGenerationPrompts.test.ts+8-2| ... | ... | @@ -195,11 +195,17 @@ describe("buildThreadTitlePrompt", () => { |
| 195 | 195 | }); |
| 196 | 196 | |
| 197 | 197 | expect(result.prompt).toContain( |
| 198 | "Generate a new title that will help the user recognize this T3 Code thread weeks later.", | |
| 198 | "Regenerate the title for an existing T3 Code thread so the user can recognize it weeks later.", | |
| 199 | 199 | ); |
| 200 | 200 | expect(result.prompt).toContain('The previous title was "Investigate reconnect regressions".'); |
| 201 | 201 | expect(result.prompt).toContain( |
| 202 | "Capture the current durable subject and outcome across the whole thread, not merely its initial request or latest step.", | |
| 202 | "Read the USER messages first. Identify the latest explicit durable goal.", | |
| 203 | ); | |
| 204 | expect(result.prompt).toContain( | |
| 205 | "Do not promote one assistant finding into the thread subject unless the user adopts it as a new goal.", | |
| 206 | ); | |
| 207 | expect(result.prompt).toContain( | |
| 208 | 'A subagent-monitoring review that finds a Codex roster bug remains "Review Subagent Monitoring Risks,"', | |
| 203 | 209 | ); |
| 204 | 210 | expect(result.prompt).toContain("Thread contents:"); |
| 205 | 211 | expect(result.prompt).toContain("The remaining issue is stale session state"); |
apps/server/src/textGeneration/TextGenerationPrompts.ts+64-31| ... | ... | @@ -153,6 +153,7 @@ interface PromptFromMessageInput { |
| 153 | 153 | guidance?: ReadonlyArray<string> | undefined; |
| 154 | 154 | rulesLabel?: string | undefined; |
| 155 | 155 | rules: ReadonlyArray<string>; |
| 156 | afterRules?: ReadonlyArray<string> | undefined; | |
| 156 | 157 | message: string; |
| 157 | 158 | messageLabel?: string | undefined; |
| 158 | 159 | preserveMessageEnd?: boolean | undefined; |
| ... | ... | @@ -182,6 +183,7 @@ function buildPromptFromMessage(input: PromptFromMessageInput): string { |
| 182 | 183 | ...(input.guidance ?? []), |
| 183 | 184 | input.rulesLabel ?? "Rules:", |
| 184 | 185 | ...input.rules.map((rule) => `- ${rule}`), |
| 186 | ...(input.afterRules ?? []), | |
| 185 | 187 | "", |
| 186 | 188 | `${input.messageLabel ?? "User message"}:`, |
| 187 | 189 | input.preserveMessageEnd |
| ... | ... | @@ -237,42 +239,73 @@ export function buildThreadTitlePrompt(input: ThreadTitlePromptInput) { |
| 237 | 239 | const prompt = buildPromptFromMessage({ |
| 238 | 240 | instruction: isRegeneration |
| 239 | 241 | ? [ |
| 240 | "Generate a new title that will help the user recognize this T3 Code thread weeks later.", | |
| 242 | "Regenerate the title for an existing T3 Code thread so the user can recognize it weeks later.", | |
| 241 | 243 | `The previous title was ${JSON.stringify(input.previousTitle)}.`, |
| 242 | 244 | ].join("\n") |
| 243 | 245 | : "Generate a title that will help the user recognize this T3 Code thread weeks later.", |
| 244 | 246 | 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 | ], | |
| 247 | guidance: isRegeneration | |
| 248 | ? [ | |
| 249 | "", | |
| 250 | "Determine the title in this order:", | |
| 251 | "1. Read the USER messages first. Identify the latest explicit durable goal. The original subject remains the subject until the user clearly changes what the thread is about.", | |
| 252 | "2. Use ASSISTANT messages to resolve vague links, unnamed code, and discovered product nouns. Do not promote one assistant finding into the thread subject unless the user adopts it as a new goal.", | |
| 253 | "3. Compare that subject with the previous title. Preserve accurate scope words, especially when earlier content is truncated. Replace the previous title when it is generic, artifact-based, a completion update, or contradicted by the thread.", | |
| 254 | "4. Title the durable subject and desired outcome, not the current workflow state.", | |
| 255 | "", | |
| 256 | ] | |
| 257 | : [ | |
| 258 | "", | |
| 259 | "Before answering, silently reduce the request to:", | |
| 260 | "- Subject: What system, feature, or problem is this really about?", | |
| 261 | "- Outcome: What does the user ultimately want to understand or change?", | |
| 262 | "- Incidental instructions: What only describes how the agent should do the work?", | |
| 263 | "", | |
| 264 | "Title the subject and outcome. Discard incidental instructions.", | |
| 265 | "", | |
| 266 | ], | |
| 255 | 267 | rulesLabel: "Editorial rules:", |
| 256 | rules: [ | |
| 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.", | |
| 269 | ...(isRegeneration | |
| 270 | ? [ | |
| 271 | "Capture the current durable subject and outcome across the whole thread, not merely its initial request or latest step.", | |
| 272 | "Return a different title from the previous title.", | |
| 273 | ] | |
| 274 | : []), | |
| 275 | ], | |
| 268 | rules: isRegeneration | |
| 269 | ? [ | |
| 270 | "3-8 words, fewer than 40 characters.", | |
| 271 | "Use a compact noun phrase or clear action phrase.", | |
| 272 | "Preserve the umbrella subject when later messages focus on one finding, provider, platform, or implementation detail.", | |
| 273 | "A thread progressing through research, planning, implementation, review, CI, merge, and monitoring has usually not changed subjects.", | |
| 274 | "Ignore deliverables and operations such as mocks, plans, HTML, branches, PRs, tests, CI, commits, merging, and monitoring unless they are the actual topic.", | |
| 275 | "Models, subagents, tools, output formats, and monitoring instructions do not belong in the title unless they are themselves the topic.", | |
| 276 | "Treat final operational follow-ups and assistant completion summaries as weak evidence of subject.", | |
| 277 | "For reviews, name the reviewed feature or system and its durable concern, not one finding from the review.", | |
| 278 | "For research, name the question domain rather than the research process.", | |
| 279 | "Do not claim the work is complete.", | |
| 280 | "Do not copy and truncate a thread message.", | |
| 281 | "Avoid project names already visible in the UI, PR numbers, quotes, labels, filler, and trailing punctuation.", | |
| 282 | "Use attached images as primary context for UI issues.", | |
| 283 | "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.", | |
| 284 | "Return a meaningfully improved title, not a cosmetic paraphrase of the previous title.", | |
| 285 | ] | |
| 286 | : [ | |
| 287 | "3-8 words, fewer than 40 characters.", | |
| 288 | "Use a compact noun phrase or clear action phrase.", | |
| 289 | "Capture the umbrella goal when the request lists several symptoms or steps.", | |
| 290 | "Name the product change, not the mock, plan, report, branch, or PR used to produce it.", | |
| 291 | "Models, subagents, tools, output formats, and monitoring instructions do not belong in the title unless they are themselves the topic.", | |
| 292 | '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.', | |
| 293 | "For research, name the question domain rather than the requested research process.", | |
| 294 | "Do not claim the work is complete.", | |
| 295 | "Do not copy and truncate the user's message.", | |
| 296 | "Avoid project names already visible in the UI, quotes, labels, filler, and trailing punctuation.", | |
| 297 | "Use attached images as primary context for UI issues.", | |
| 298 | "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.", | |
| 299 | ], | |
| 300 | afterRules: isRegeneration | |
| 301 | ? [ | |
| 302 | "", | |
| 303 | "Examples of the distinction:", | |
| 304 | '- A subagent-monitoring review that finds a Codex roster bug remains "Review Subagent Monitoring Risks," not "Codex Roster Bug Review."', | |
| 305 | '- A vague failing-test request later identified as a lazy thread-feed mismatch becomes "Fix Lazy Thread Feed Test," not "Prevent Mobile Feed Regressions."', | |
| 306 | "- A QR-sharing overhaul that ends with CI and merge work remains about QR sharing, not the PR lifecycle.", | |
| 307 | ] | |
| 308 | : undefined, | |
| 276 | 309 | message: input.message, |
| 277 | 310 | ...(isRegeneration |
| 278 | 311 | ? { |