diff --git a/apps/server/src/project/AgentSessionScanner.test.ts b/apps/server/src/project/AgentSessionScanner.test.ts index c2481eb35489c..797fdf0292584 100644 --- a/apps/server/src/project/AgentSessionScanner.test.ts +++ b/apps/server/src/project/AgentSessionScanner.test.ts @@ -3057,7 +3057,7 @@ describe("parseAgentSessionTranscript", () => { lastActiveAtMs: Date.parse("2026-08-25T08:00:00.000Z"), }); - expect(thread?.title).toBe(""); + expect(thread?.title).toBe("Initialize Git and add a README."); expect(thread?.messages.map((message) => message.text)).toEqual([ context, "Initialize Git and add a README.", @@ -3084,7 +3084,7 @@ describe("parseAgentSessionTranscript", () => { lastActiveAtMs: Date.parse("2026-08-25T08:00:00.000Z"), }); - expect(thread?.title).toBe(""); + expect(thread?.title).toBe("Create a useful project."); expect(thread?.messages.map((message) => message.text)).toEqual([prompt]); }); @@ -3107,7 +3107,32 @@ describe("parseAgentSessionTranscript", () => { lastActiveAtMs: Date.parse("2026-08-25T08:00:00.000Z"), }); - expect(thread?.title).toBe("## My request for Codex:"); + expect(thread?.title).toBe("Fix the visible bug"); + expect(thread?.messages.map((message) => message.text)).toEqual([prompt]); + }); + + it("titles a Codex event after the request that follows injected setup blocks", () => { + const prompt = [ + "\nGitHub\n", + "# AGENTS.md instructions for /tmp/project\n\n\nRules\n", + '\nTab\n', + "Fix the import titles", + ].join("\n"); + const thread = AgentSessionScanner.parseAgentSessionTranscript({ + contents: [ + encodeTranscriptRecord({ type: "session_meta", payload: { id: "codex-session" } }), + encodeTranscriptRecord({ + type: "event_msg", + payload: { type: "user_message", message: prompt }, + }), + ].join("\n"), + source: "codex", + providerInstanceId: ProviderInstanceId.make("codex"), + fallbackSessionId: "fallback", + lastActiveAtMs: Date.parse("2026-08-25T08:00:00.000Z"), + }); + + expect(thread?.title).toBe("Fix the import titles"); expect(thread?.messages.map((message) => message.text)).toEqual([prompt]); }); diff --git a/apps/server/src/project/AgentSessionScanner.ts b/apps/server/src/project/AgentSessionScanner.ts index 368309b6641fd..5b22f874b6eb3 100644 --- a/apps/server/src/project/AgentSessionScanner.ts +++ b/apps/server/src/project/AgentSessionScanner.ts @@ -247,6 +247,16 @@ function codexTurnId(metadata: unknown): string | null { return decoded.value.turn_id; } +// Codex records setup context as user text ahead of the real request. Skip +// these leading blocks when deriving a title; imported messages keep them. +const CODEX_TITLE_PREAMBLE = + /^(?:\s*(?:<(recommended_plugins|environment_context|user_instructions|INSTRUCTIONS|in-app-browser-context)(?:\s[^>]*)?>[\s\S]*?<\/\1>|# AGENTS\.md instructions\b[^\n]*|## My request for Codex:))+/; + +function deriveTitle(source: AgentSessionSource, text: string): string | undefined { + const request = source === "codex" ? text.replace(CODEX_TITLE_PREAMBLE, "") : text; + return request.trim().split("\n")[0]?.slice(0, 100).trim() || undefined; +} + export function parseAgentSessionTranscript( input: AgentSessionTranscriptMetadata & { readonly contents: string; @@ -267,6 +277,7 @@ function parseAgentSessionRecords( // timestamp text, so only transcript metadata can provide a resumable ID. let providerSessionId = input.source === "codex" ? "" : input.fallbackSessionId; let title: string | null = null; + let derivedTitle: string | undefined; let model: string | null = null; let hasCodexSessionId = false; const messages: Array = []; @@ -332,6 +343,9 @@ function parseAgentSessionRecords( if (firstUserMessage === undefined && message.role === "user") { firstUserMessage = message; } + if (derivedTitle === undefined && message.role === "user") { + derivedTitle = deriveTitle(input.source, message.text); + } messages.push(message); if (messages.length > MAX_IMPORTED_MESSAGES) messages.shift(); }; @@ -452,13 +466,12 @@ function parseAgentSessionRecords( const retainedMessages = firstUserMessageRetained ? visibleMessages : [visibleFirstUserMessage, ...visibleMessages.slice(-(MAX_IMPORTED_MESSAGES - 1))]; - const derivedTitle = visibleFirstUserMessage.text.trim().split("\n")[0]?.slice(0, 100).trim(); return { source: input.source, providerInstanceId: input.providerInstanceId, providerSessionId, - title: title ?? (derivedTitle && derivedTitle.length > 0 ? derivedTitle : "Imported thread"), + title: title ?? derivedTitle ?? "Imported thread", model, createdAt: retainedMessages[0]?.createdAt ?? fallbackTimestamp, updatedAt: fallbackTimestamp,