Skip to content

Commit 5253a17

Browse files
committed
fix(server): derive imported Codex titles from user requests
1 parent 8b2838e commit 5253a17

2 files changed

Lines changed: 74 additions & 7 deletions

File tree

‎apps/server/src/project/AgentSessionScanner.test.ts‎

Lines changed: 56 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1470,7 +1470,7 @@ it.layer(NodeServices.layer)("AgentSessionScanner", (it) => {
14701470
contents: [
14711471
encodeTranscriptRecord({
14721472
type: "session_meta",
1473-
payload: { id: "codex-recent", cwd: workspace },
1473+
payload: { id: "codex-recent", cwd: workspace, name: "Saved code review" },
14741474
}),
14751475
encodeTranscriptRecord({
14761476
type: "event_msg",
@@ -1500,6 +1500,10 @@ it.layer(NodeServices.layer)("AgentSessionScanner", (it) => {
15001500
"codex-recent",
15011501
"claude-recent",
15021502
]);
1503+
expect(threads.map((thread) => thread.title)).toEqual([
1504+
"Saved code review",
1505+
"Fix the project",
1506+
]);
15031507
expect(threads.map((thread) => thread.messages.map((message) => message.text))).toEqual([
15041508
["Review this code", "Looks good"],
15051509
["Fix the project", "Done"],
@@ -2958,7 +2962,7 @@ describe("parseAgentSessionTranscript", () => {
29582962
}),
29592963
encodeTranscriptRecord({
29602964
type: "session_meta",
2961-
payload: { id: "parent-session" },
2965+
payload: { id: "parent-session", name: "Parent title" },
29622966
}),
29632967
encodeTranscriptRecord({
29642968
type: "event_msg",
@@ -2972,6 +2976,7 @@ describe("parseAgentSessionTranscript", () => {
29722976
});
29732977

29742978
expect(thread?.providerSessionId).toBe("fork-session");
2979+
expect(thread?.title).toBe("Continue in the fork");
29752980
});
29762981

29772982
it("skips Codex transcripts without a resumable session ID", () => {
@@ -3064,6 +3069,52 @@ describe("parseAgentSessionTranscript", () => {
30643069
]);
30653070
});
30663071

3072+
it.each([
3073+
[
3074+
"<recommended_plugins>\nAvailable plugins\n</recommended_plugins>Fix the import",
3075+
{},
3076+
"Fix the import",
3077+
],
3078+
[
3079+
"<recommended_plugins>\nPlugins\n</recommended_plugins>\n<environment_context>\n<cwd>/tmp</cwd>\n</environment_context>\n## My request for Codex:\n\nFix the import",
3080+
{},
3081+
"Fix the import",
3082+
],
3083+
[
3084+
"# AGENTS.md instructions for /tmp\n\n<INSTRUCTIONS>\nRules\n</INSTRUCTIONS>\n<user_instructions>Rules</user_instructions>\nFix the import",
3085+
{},
3086+
"Fix the import",
3087+
],
3088+
["<recommended_plugins>Plugins</recommended_plugins>", {}, "Imported thread"],
3089+
["<recommended_plugins>Unclosed example", {}, "<recommended_plugins>Unclosed example"],
3090+
["<example>Keep this XML</example>", {}, "<example>Keep this XML</example>"],
3091+
["Fix the import\nMore detail", { name: " Saved title " }, "Saved title"],
3092+
["Fix the import", { threadName: "Renamed thread" }, "Renamed thread"],
3093+
["Fix the import", { name: " ", threadName: " " }, "Fix the import"],
3094+
["Fix the import", { name: null, threadName: null }, "Fix the import"],
3095+
["x".repeat(120), {}, "x".repeat(100)],
3096+
])("derives a Codex title without changing prompt %s", (prompt, metadata, title) => {
3097+
const thread = AgentSessionScanner.parseAgentSessionTranscript({
3098+
contents: [
3099+
encodeTranscriptRecord({
3100+
type: "session_meta",
3101+
payload: { id: "codex-session", ...metadata },
3102+
}),
3103+
encodeTranscriptRecord({
3104+
type: "event_msg",
3105+
payload: { type: "user_message", message: prompt },
3106+
}),
3107+
].join("\n"),
3108+
source: "codex",
3109+
providerInstanceId: ProviderInstanceId.make("codex"),
3110+
fallbackSessionId: "fallback",
3111+
lastActiveAtMs: Date.parse("2026-08-25T08:00:00.000Z"),
3112+
});
3113+
3114+
expect(thread?.title).toBe(title);
3115+
expect(thread?.messages.map((message) => message.text)).toEqual([prompt]);
3116+
});
3117+
30673118
it("preserves context markup in response-only Codex messages", () => {
30683119
const context = "<environment_context>\n<cwd>/tmp/project</cwd>\n</environment_context>";
30693120
const thread = AgentSessionScanner.parseAgentSessionTranscript({
@@ -3097,7 +3148,7 @@ describe("parseAgentSessionTranscript", () => {
30973148
lastActiveAtMs: Date.parse("2026-08-25T08:00:00.000Z"),
30983149
});
30993150

3100-
expect(thread?.title).toBe("<environment_context>");
3151+
expect(thread?.title).toBe("Initialize Git and add a README.");
31013152
expect(thread?.messages.map((message) => message.text)).toEqual([
31023153
context,
31033154
"Initialize Git and add a README.",
@@ -3124,7 +3175,7 @@ describe("parseAgentSessionTranscript", () => {
31243175
lastActiveAtMs: Date.parse("2026-08-25T08:00:00.000Z"),
31253176
});
31263177

3127-
expect(thread?.title).toBe("<environment_context>");
3178+
expect(thread?.title).toBe("Create a useful project.");
31283179
expect(thread?.messages.map((message) => message.text)).toEqual([prompt]);
31293180
});
31303181

@@ -3147,7 +3198,7 @@ describe("parseAgentSessionTranscript", () => {
31473198
lastActiveAtMs: Date.parse("2026-08-25T08:00:00.000Z"),
31483199
});
31493200

3150-
expect(thread?.title).toBe("## My request for Codex:");
3201+
expect(thread?.title).toBe("Fix the visible bug");
31513202
expect(thread?.messages.map((message) => message.text)).toEqual([prompt]);
31523203
});
31533204

‎apps/server/src/project/AgentSessionScanner.ts‎

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,8 @@ const TranscriptRecord = Schema.Struct({
123123
Schema.Struct({
124124
id: Schema.optional(Schema.String),
125125
session_id: Schema.optional(Schema.String),
126+
name: Schema.optional(Schema.NullOr(Schema.String)),
127+
threadName: Schema.optional(Schema.NullOr(Schema.String)),
126128
type: Schema.optional(Schema.String),
127129
role: Schema.optional(Schema.String),
128130
message: Schema.optional(Schema.String),
@@ -303,6 +305,7 @@ function parseAgentSessionRecords(
303305
// timestamp text, so only transcript metadata can provide a resumable ID.
304306
let providerSessionId = input.source === "codex" ? "" : input.fallbackSessionId;
305307
let title: string | null = null;
308+
let derivedTitle: string | undefined;
306309
let model: string | null = null;
307310
let hasCodexSessionId = false;
308311
const messages: Array<AgentSessionThreadMessage & { readonly codexResponseUser: boolean }> = [];
@@ -371,6 +374,19 @@ function parseAgentSessionRecords(
371374
if (firstUserMessage === undefined && message.role === "user") {
372375
firstUserMessage = message;
373376
}
377+
if (derivedTitle === undefined && message.role === "user") {
378+
// Strip only known leading Codex preambles, and only for the title.
379+
const prompt =
380+
input.source === "codex"
381+
? message.text
382+
.trim()
383+
.replace(
384+
/^(?:(?:# AGENTS\.md instructions for [^\n]*\n\s*)?<(recommended_plugins|environment_context|user_instructions|INSTRUCTIONS)>[\s\S]*?<\/\1>\s*|## My request for Codex:\s*)+/,
385+
"",
386+
)
387+
: message.text.trim();
388+
derivedTitle = prompt.split("\n")[0]?.slice(0, 100).trim() || undefined;
389+
}
374390
messages.push(message);
375391
if (messages.length > MAX_IMPORTED_MESSAGES) messages.shift();
376392
};
@@ -428,6 +444,7 @@ function parseAgentSessionRecords(
428444
if (!hasCodexSessionId && sessionId) {
429445
providerSessionId = sessionId;
430446
hasCodexSessionId = true;
447+
title = record.payload?.name?.trim() || record.payload?.threadName?.trim() || null;
431448
}
432449
continue;
433450
}
@@ -491,13 +508,12 @@ function parseAgentSessionRecords(
491508
const retainedMessages = firstUserMessageRetained
492509
? visibleMessages
493510
: [visibleFirstUserMessage, ...visibleMessages.slice(-(MAX_IMPORTED_MESSAGES - 1))];
494-
const derivedTitle = visibleFirstUserMessage.text.trim().split("\n")[0]?.slice(0, 100).trim();
495511

496512
return {
497513
source: input.source,
498514
providerInstanceId: input.providerInstanceId,
499515
providerSessionId,
500-
title: title ?? (derivedTitle && derivedTitle.length > 0 ? derivedTitle : "Imported thread"),
516+
title: title ?? derivedTitle ?? "Imported thread",
501517
model,
502518
createdAt: retainedMessages[0]?.createdAt ?? fallbackTimestamp,
503519
updatedAt: fallbackTimestamp,

0 commit comments

Comments
 (0)