Summary
parseTranscriptMessages in apps/web/lib/plugin-document.ts builds its message-splitting regex with the m flag, and the terminating lookahead ends with |$:
const regex = new RegExp(
`^\\s*(\\d+)\\.\\s+\\[(${TRANSCRIPT_ROLE_PATTERN})\\]\\s*([\\s\\S]*?)(?=^\\s*\\d+\\.\\s+\\[(?:${TRANSCRIPT_ROLE_PATTERN})\\]\\s*|$)`,
"gm",
)
With m, $ matches at every line end, not just end of input, so the lazy ([\s\S]*?) body — clearly written to span lines — stops at the first newline. Every line of a message after the first is silently dropped.
Reproduction
const content = [
"1. [user] Hello there",
"Here is more context on line two",
"memory id: mem_123",
"2. [assistant] Sure!",
"Second line of the reply",
].join("\n")
Actual parsed messages: "Hello there" and "Sure!". The continuation lines appear in no message.
Impact
- Codex session and Amp thread documents (
parseSessionTranscript) render truncated conversations in the document modal — only the first line of each turn survives.
memory id: ... lines inside a message body are dropped instead of being surfaced as artifacts by extractArtifacts, so the Memory ID chip never shows for multi-line turns.
- Session previews and user/assistant message counts are computed from truncated text.
This is amplified by normalizeContent, which converts literal \n escapes into real newlines before parsing — so multi-line bodies are the norm for these payloads, not the edge case.
Expected behavior
Message bodies should span lines until the next N. [role] header or the true end of input. The last message in a transcript should keep all of its lines.
Suggested fix
Replace the |$ alternative in the lookahead with a real end-of-input assertion that isn't affected by the m flag: |(?![\s\S]). One-line change; the adjacent parseClaudeCodeTurns regex is unaffected (it uses $ without the m flag, where it already means end of input).
Summary
parseTranscriptMessagesinapps/web/lib/plugin-document.tsbuilds its message-splitting regex with themflag, and the terminating lookahead ends with|$:With
m,$matches at every line end, not just end of input, so the lazy([\s\S]*?)body — clearly written to span lines — stops at the first newline. Every line of a message after the first is silently dropped.Reproduction
Actual parsed messages:
"Hello there"and"Sure!". The continuation lines appear in no message.Impact
parseSessionTranscript) render truncated conversations in the document modal — only the first line of each turn survives.memory id: ...lines inside a message body are dropped instead of being surfaced as artifacts byextractArtifacts, so the Memory ID chip never shows for multi-line turns.This is amplified by
normalizeContent, which converts literal\nescapes into real newlines before parsing — so multi-line bodies are the norm for these payloads, not the edge case.Expected behavior
Message bodies should span lines until the next
N. [role]header or the true end of input. The last message in a transcript should keep all of its lines.Suggested fix
Replace the
|$alternative in the lookahead with a real end-of-input assertion that isn't affected by themflag:|(?![\s\S]). One-line change; the adjacentparseClaudeCodeTurnsregex is unaffected (it uses$without themflag, where it already means end of input).