From fa7751eeda33771d4a575f765fb7b87c17a1890f Mon Sep 17 00:00:00 2001 From: MarcoDotIO Date: Wed, 4 Nov 2026 11:25:39 -0500 Subject: [PATCH] fix(server): imported thread titles no longer end in a broken emoji Titles derived from an imported session's first message were cut at 100 UTF-16 code units, which could split an emoji's surrogate pair and leave a lone half that persists as U+FFFD. Back off one unit when the cut would land inside a pair. Co-Authored-By: Claude Opus 5.5 --- .../src/project/AgentSessionScanner.test.ts | 19 +++++++++++++++++++ .../server/src/project/AgentSessionScanner.ts | 9 ++++++++- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/apps/server/src/project/AgentSessionScanner.test.ts b/apps/server/src/project/AgentSessionScanner.test.ts index ce1830b8c5710..709d230c07887 100644 --- a/apps/server/src/project/AgentSessionScanner.test.ts +++ b/apps/server/src/project/AgentSessionScanner.test.ts @@ -2741,6 +2741,25 @@ describe("parseAgentSessionTranscript", () => { expect(thread?.messages.map((message) => message.text)).toEqual([prompt]); }); + it("does not split an emoji when shortening a long first message into a title", () => { + const prompt = `${"a".repeat(99)}🚀 then deploy`; + 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("a".repeat(99)); + }); + 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 3ed97f8a7d8f4..40b6d7bf3b603 100644 --- a/apps/server/src/project/AgentSessionScanner.ts +++ b/apps/server/src/project/AgentSessionScanner.ts @@ -335,9 +335,16 @@ function parseCodexThreadNames(contents: string): ReadonlyMap { return names; } +const MAX_DERIVED_TITLE_LENGTH = 100; + 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; + const line = request.trim().split("\n")[0] ?? ""; + // Cutting between an emoji's surrogates leaves a lone half that persists as U+FFFD. + const last = line.charCodeAt(MAX_DERIVED_TITLE_LENGTH - 1); + const end = + last >= 0xd800 && last <= 0xdbff ? MAX_DERIVED_TITLE_LENGTH - 1 : MAX_DERIVED_TITLE_LENGTH; + return line.slice(0, end).trim() || undefined; } // Pi keeps abandoned branches in the file; its active branch runs from the last entry to the root.