| 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,6 +785,123 @@ describe("ProviderCommandReactor", () => { |
| 785 | expect(thread?.titleRegeneration).toBeNull(); | 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 | it("clears title regeneration state left pending across reactor startup", async () => { | 905 | it("clears title regeneration state left pending across reactor startup", async () => { |
| 789 | const harness = await createHarness({ | 906 | const harness = await createHarness({ |
| 790 | titleRegenerationBeforeStart: "one", | 907 | titleRegenerationBeforeStart: "one", |
| ... | @@ -972,10 +1089,14 @@ describe("ProviderCommandReactor", () => { | ... | @@ -972,10 +1089,14 @@ describe("ProviderCommandReactor", () => { |
| 972 | expect(thread?.titleRegeneration).toBeNull(); | 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 | const harness = await createHarness(); | 1093 | const harness = await createHarness(); |
| 977 | const now = "2026-01-01T00:00:00.000Z"; | 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 | await harness.runEffect( | 1101 | await harness.runEffect( |
| 981 | harness.engine.dispatch({ | 1102 | harness.engine.dispatch({ |
| ... | @@ -1040,9 +1161,14 @@ describe("ProviderCommandReactor", () => { | ... | @@ -1040,9 +1161,14 @@ describe("ProviderCommandReactor", () => { |
| 1040 | await harness.drain(); | 1161 | await harness.drain(); |
| 1041 | 1162 | ||
| 1042 | expect(harness.generateThreadTitle.mock.calls[0]?.[0].message).toBe( | 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 | it("does not overwrite a manual rename while title regeneration is running", async () => { | 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,41 +94,61 @@ const DEFAULT_RUNTIME_MODE: RuntimeMode = "full-access"; |
| 94 | const DEFAULT_THREAD_TITLE = "New thread"; | 94 | const DEFAULT_THREAD_TITLE = "New thread"; |
| 95 | const MAX_REGENERATION_ATTACHMENTS = 4; | 95 | const MAX_REGENERATION_ATTACHMENTS = 4; |
| 96 | const MAX_THREAD_TITLE_CONTEXT_CHARS = 8_000; | 96 | const MAX_THREAD_TITLE_CONTEXT_CHARS = 8_000; |
| 97 | const MAX_FIRST_USER_TITLE_CONTEXT_CHARS = 2_000; | ||
| 97 | const THREAD_TITLE_CONTEXT_TRUNCATION_MARKER = "[Earlier content truncated]\n\n"; | 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( | 101 | type ThreadTitleMessage = { |
| 100 | messages: ReadonlyArray<{ | 102 | readonly role: "user" | "assistant" | "system"; |
| 101 | readonly role: "user" | "assistant" | "system"; | 103 | readonly text: string; |
| 102 | readonly text: string; | 104 | readonly attachments?: ReadonlyArray<ChatAttachment> | undefined; |
| 103 | readonly attachments?: ReadonlyArray<ChatAttachment> | undefined; | 105 | }; |
| 104 | }>, | 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 | readonly attachments: ReadonlyArray<ChatAttachment>; | 137 | readonly attachments: ReadonlyArray<ChatAttachment>; |
| 138 | readonly truncated: boolean; | ||
| 108 | } { | 139 | } { |
| 109 | let context = ""; | 140 | let context = ""; |
| 110 | let truncated = false; | 141 | let truncated = false; |
| 111 | const retainedAttachments: Array<ChatAttachment> = []; | 142 | const retainedAttachments: Array<ChatAttachment> = []; |
| 112 | 143 | ||
| 113 | for (const message of messages.toReversed()) { | 144 | for (const message of messages.toReversed()) { |
| 114 | if (message.role === "system") { | 145 | const section = formatThreadTitleSection(message); |
| 115 | continue; | 146 | if (section === undefined) { |
| 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) { | ||
| 126 | continue; | 147 | continue; |
| 127 | } | 148 | } |
| 128 | 149 | ||
| 129 | const section = `${message.role.toUpperCase()}:\n${contents}`; | ||
| 130 | const separator = context.length > 0 ? "\n\n" : ""; | 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 | if (section.length > available) { | 152 | if (section.length > available) { |
| 133 | if (available > 0) { | 153 | if (available > 0) { |
| 134 | context = `${section.slice(-available)}${separator}${context}`; | 154 | context = `${section.slice(-available)}${separator}${context}`; |
| ... | @@ -141,9 +161,54 @@ function formatThreadTitleContext( | ... | @@ -141,9 +161,54 @@ function formatThreadTitleContext( |
| 141 | retainedAttachments.unshift(...(message.attachments ?? [])); | 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 | return { | 204 | return { |
| 145 | message: truncated ? `${THREAD_TITLE_CONTEXT_TRUNCATION_MARKER}${context}` : context, | 205 | message: `${pinnedSection}\n\n${THREAD_TITLE_CONTEXT_TRUNCATION_MARKER}${retainedRecent.context}`, |
| 146 | attachments: retainedAttachments.slice(-MAX_REGENERATION_ATTACHMENTS), | 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,11 +195,17 @@ describe("buildThreadTitlePrompt", () => { |
| 195 | }); | 195 | }); |
| 196 | 196 | ||
| 197 | expect(result.prompt).toContain( | 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 | expect(result.prompt).toContain('The previous title was "Investigate reconnect regressions".'); | 200 | expect(result.prompt).toContain('The previous title was "Investigate reconnect regressions".'); |
| 201 | expect(result.prompt).toContain( | 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 | expect(result.prompt).toContain("Thread contents:"); | 210 | expect(result.prompt).toContain("Thread contents:"); |
| 205 | expect(result.prompt).toContain("The remaining issue is stale session state"); | 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,6 +153,7 @@ interface PromptFromMessageInput { |
| 153 | guidance?: ReadonlyArray<string> | undefined; | 153 | guidance?: ReadonlyArray<string> | undefined; |
| 154 | rulesLabel?: string | undefined; | 154 | rulesLabel?: string | undefined; |
| 155 | rules: ReadonlyArray<string>; | 155 | rules: ReadonlyArray<string>; |
| 156 | afterRules?: ReadonlyArray<string> | undefined; | ||
| 156 | message: string; | 157 | message: string; |
| 157 | messageLabel?: string | undefined; | 158 | messageLabel?: string | undefined; |
| 158 | preserveMessageEnd?: boolean | undefined; | 159 | preserveMessageEnd?: boolean | undefined; |
| ... | @@ -182,6 +183,7 @@ function buildPromptFromMessage(input: PromptFromMessageInput): string { | ... | @@ -182,6 +183,7 @@ function buildPromptFromMessage(input: PromptFromMessageInput): string { |
| 182 | ...(input.guidance ?? []), | 183 | ...(input.guidance ?? []), |
| 183 | input.rulesLabel ?? "Rules:", | 184 | input.rulesLabel ?? "Rules:", |
| 184 | ...input.rules.map((rule) => `- ${rule}`), | 185 | ...input.rules.map((rule) => `- ${rule}`), |
| 186 | ...(input.afterRules ?? []), | ||
| 185 | "", | 187 | "", |
| 186 | `${input.messageLabel ?? "User message"}:`, | 188 | `${input.messageLabel ?? "User message"}:`, |
| 187 | input.preserveMessageEnd | 189 | input.preserveMessageEnd |
| ... | @@ -237,42 +239,73 @@ export function buildThreadTitlePrompt(input: ThreadTitlePromptInput) { | ... | @@ -237,42 +239,73 @@ export function buildThreadTitlePrompt(input: ThreadTitlePromptInput) { |
| 237 | const prompt = buildPromptFromMessage({ | 239 | const prompt = buildPromptFromMessage({ |
| 238 | instruction: isRegeneration | 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 | `The previous title was ${JSON.stringify(input.previousTitle)}.`, | 243 | `The previous title was ${JSON.stringify(input.previousTitle)}.`, |
| 242 | ].join("\n") | 244 | ].join("\n") |
| 243 | : "Generate a title that will help the user recognize this T3 Code thread weeks later.", | 245 | : "Generate a title that will help the user recognize this T3 Code thread weeks later.", |
| 244 | responseShape: "Return JSON with exactly one key: title.", | 246 | responseShape: "Return JSON with exactly one key: title.", |
| 245 | guidance: [ | 247 | guidance: isRegeneration |
| 246 | "", | 248 | ? [ |
| 247 | "Before answering, silently reduce the request to:", | 249 | "", |
| 248 | "- Subject: What system, feature, or problem is this really about?", | 250 | "Determine the title in this order:", |
| 249 | "- Outcome: What does the user ultimately want to understand or change?", | 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.", |
| 250 | "- Incidental instructions: What only describes how the agent should do the work?", | 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.", |
| 251 | "", | 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.", |
| 252 | "Title the subject and outcome. Discard incidental instructions.", | 254 | "4. Title the durable subject and desired outcome, not the current workflow state.", |
| 253 | "", | 255 | "", |
| 254 | ], | 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 | rulesLabel: "Editorial rules:", | 267 | rulesLabel: "Editorial rules:", |
| 256 | rules: [ | 268 | rules: isRegeneration |
| 257 | "3-8 words, fewer than 40 characters.", | 269 | ? [ |
| 258 | "Use a compact noun phrase or clear action phrase.", | 270 | "3-8 words, fewer than 40 characters.", |
| 259 | "Capture the umbrella goal when the request lists several symptoms or steps.", | 271 | "Use a compact noun phrase or clear action phrase.", |
| 260 | "Name the product change, not the mock, plan, report, branch, or PR used to produce it.", | 272 | "Preserve the umbrella subject when later messages focus on one finding, provider, platform, or implementation detail.", |
| 261 | "Models, subagents, tools, output formats, and monitoring instructions do not belong in the title unless they are themselves the topic.", | 273 | "A thread progressing through research, planning, implementation, review, CI, merge, and monitoring has usually not changed subjects.", |
| 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.', | 274 | "Ignore deliverables and operations such as mocks, plans, HTML, branches, PRs, tests, CI, commits, merging, and monitoring unless they are the actual topic.", |
| 263 | "For research, name the question domain rather than the requested research process.", | 275 | "Models, subagents, tools, output formats, and monitoring instructions do not belong in the title unless they are themselves the topic.", |
| 264 | "Do not claim the work is complete.", | 276 | "Treat final operational follow-ups and assistant completion summaries as weak evidence of subject.", |
| 265 | "Do not copy and truncate the user's message.", | 277 | "For reviews, name the reviewed feature or system and its durable concern, not one finding from the review.", |
| 266 | "Avoid project names already visible in the UI, quotes, labels, filler, and trailing punctuation.", | 278 | "For research, name the question domain rather than the research process.", |
| 267 | "Use attached images as primary context for UI issues.", | 279 | "Do not claim the work is complete.", |
| 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.", | 280 | "Do not copy and truncate a thread message.", |
| 269 | ...(isRegeneration | 281 | "Avoid project names already visible in the UI, PR numbers, quotes, labels, filler, and trailing punctuation.", |
| 270 | ? [ | 282 | "Use attached images as primary context for UI issues.", |
| 271 | "Capture the current durable subject and outcome across the whole thread, not merely its initial request or latest step.", | 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.", |
| 272 | "Return a different title from the previous title.", | 284 | "Return a meaningfully improved title, not a cosmetic paraphrase of the previous title.", |
| 273 | ] | 285 | ] |
| 274 | : []), | 286 | : [ |
| 275 | ], | 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 | message: input.message, | 309 | message: input.message, |
| 277 | ...(isRegeneration | 310 | ...(isRegeneration |
| 278 | ? { | 311 | ? { |