diff --git a/apps/server/src/project/AgentSessionScanner.test.ts b/apps/server/src/project/AgentSessionScanner.test.ts
index 688b974e7658d..8dbf948d6eccd 100644
--- a/apps/server/src/project/AgentSessionScanner.test.ts
+++ b/apps/server/src/project/AgentSessionScanner.test.ts
@@ -2382,11 +2382,16 @@ describe("parseAgentSessionTranscript", () => {
]);
});
- it("titles a Codex event after the request that follows injected setup blocks", () => {
+ it.each([
+ ['{"page_id":null}'],
+ ['{"page_id":"page-1"}'],
+ [""],
+ ])("titles a Codex event after the request that follows injected setup blocks: %s", (page) => {
const prompt = [
"\nGitHub\n",
"# AGENTS.md instructions for /tmp/project\n\n\nRules\n",
'\nTab\n',
+ page,
"Fix the import titles",
].join("\n");
const thread = AgentSessionScanner.parseAgentSessionTranscript({
@@ -2407,6 +2412,43 @@ describe("parseAgentSessionTranscript", () => {
expect(thread?.messages.map((message) => message.text)).toEqual([prompt]);
});
+ it.each([
+ {
+ prompts: [
+ '{"page_id":null}',
+ "Import my sessions",
+ ],
+ title: "Import my sessions",
+ },
+ {
+ prompts: ["Explain in this transcript"],
+ title: "Explain in this transcript",
+ },
+ {
+ prompts: ["Keep this incomplete example"],
+ title: "Keep this incomplete example",
+ },
+ ])("derives a Codex title without discarding user text: $title", ({ prompts, title }) => {
+ const thread = AgentSessionScanner.parseAgentSessionTranscript({
+ contents: [
+ encodeTranscriptRecord({ type: "session_meta", payload: { id: "codex-session" } }),
+ ...prompts.map((message) =>
+ encodeTranscriptRecord({
+ type: "event_msg",
+ payload: { type: "user_message", message },
+ }),
+ ),
+ ].join("\n"),
+ source: "codex",
+ providerInstanceId: ProviderInstanceId.make("codex"),
+ fallbackSessionId: "fallback",
+ lastActiveAtMs: Date.parse("2026-08-25T08:00:00.000Z"),
+ });
+
+ expect(thread?.title).toBe(title);
+ expect(thread?.messages.map((message) => message.text)).toEqual(prompts);
+ });
+
it("skips sessions without a visible user message", () => {
const thread = AgentSessionScanner.parseAgentSessionTranscript({
contents: JSON.stringify({
diff --git a/apps/server/src/project/AgentSessionScanner.ts b/apps/server/src/project/AgentSessionScanner.ts
index bbd3ba5a631e0..dabe6ce855829 100644
--- a/apps/server/src/project/AgentSessionScanner.ts
+++ b/apps/server/src/project/AgentSessionScanner.ts
@@ -305,7 +305,7 @@ function foldClaudeFields(fields: ClaudeFields, record: DecodedTranscriptRecord)
// 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:))+/;
+ /^(?:\s*(?:<(recommended_plugins|environment_context|user_instructions|INSTRUCTIONS|in-app-browser-context|external_codex_apps_open_page)(?:\s[^>]*)?>[\s\S]*?<\/\1>|# AGENTS\.md instructions\b[^\n]*|## My request for Codex:))+/;
// Codex appends an entry per rename; like Codex, the newest nonempty name wins.
function parseCodexThreadNames(contents: string): ReadonlyMap {