@@ -44,7 +44,13 @@ export function parseChatMessageRow(row: unknown): {
4444 * tool_metadata and replaces it with a `contentSize` byte count.
4545 * This dramatically reduces RPC payload size for tool-heavy sessions.
4646 */
47- export function parseChatMessageRowCompact ( row : unknown ) : {
47+ export type CompactMessageOptions = {
48+ documentCardRawOutputMaxBytes ?: number ;
49+ } ;
50+
51+ export const DEFAULT_DOCUMENT_CARD_RAW_OUTPUT_MAX_BYTES = 16 * 1024 ;
52+
53+ export function parseChatMessageRowCompact ( row : unknown , options ?: CompactMessageOptions ) : {
4854 id : string ;
4955 sessionId : string ;
5056 role : string ;
@@ -55,7 +61,7 @@ export function parseChatMessageRowCompact(row: unknown): {
5561} {
5662 const r = parseRow ( ChatMessageRowSchema , row , 'chat_message' ) ;
5763 const parsed = safeParseJson ( r . tool_metadata ) ;
58- const toolMetadata = parsed === null ? null : stripToolMetadataContent ( parsed ) ;
64+ const toolMetadata = parsed === null ? null : stripToolMetadataContent ( parsed , options ) ;
5965 return {
6066 id : r . id ,
6167 sessionId : r . session_id ,
@@ -73,18 +79,135 @@ export function parseChatMessageRowCompact(row: unknown): {
7379 * Preserves all other metadata fields (toolCallId, title, kind, status, locations).
7480 */
7581const textEncoder = new TextEncoder ( ) ;
82+ const DOCUMENT_CARD_TOOLS = new Set ( [
83+ 'upload_to_library' ,
84+ 'replace_library_file' ,
85+ 'display_from_library' ,
86+ ] ) ;
87+ const TOOL_NAME_SEPARATORS = / _ _ | \/ | \. | : / ;
88+
89+ function resolveDocumentCardRawOutputMaxBytes ( options ?: CompactMessageOptions ) : number {
90+ const configured = options ?. documentCardRawOutputMaxBytes ;
91+ return typeof configured === 'number' && Number . isFinite ( configured ) && configured > 0
92+ ? Math . floor ( configured )
93+ : DEFAULT_DOCUMENT_CARD_RAW_OUTPUT_MAX_BYTES ;
94+ }
95+
96+ function normalizeToolName ( toolName : unknown ) : string | undefined {
97+ if ( typeof toolName !== 'string' || ! toolName ) return undefined ;
98+ const segments = toolName . split ( TOOL_NAME_SEPARATORS ) . filter ( Boolean ) ;
99+ return segments . length > 0 ? segments [ segments . length - 1 ] : toolName ;
100+ }
101+
102+ function isDocumentCardTool ( meta : Record < string , unknown > ) : boolean {
103+ const base = normalizeToolName ( meta . toolName ) ?? normalizeToolName ( meta . title ) ;
104+ return Boolean ( base && DOCUMENT_CARD_TOOLS . has ( base ) ) ;
105+ }
106+
107+ function isRecord ( value : unknown ) : value is Record < string , unknown > {
108+ return typeof value === 'object' && value !== null ;
109+ }
110+
111+ function parseDocumentPayloadCandidate (
112+ value : string ,
113+ maxBytes : number
114+ ) : Record < string , unknown > | undefined {
115+ const trimmed = value . trim ( ) ;
116+ if ( ! trimmed || textEncoder . encode ( trimmed ) . byteLength > maxBytes ) {
117+ return undefined ;
118+ }
119+
120+ const candidates = [ trimmed ] ;
121+ const firstBrace = trimmed . indexOf ( '{' ) ;
122+ const lastBrace = trimmed . lastIndexOf ( '}' ) ;
123+ if ( firstBrace >= 0 && lastBrace > firstBrace ) {
124+ candidates . push ( trimmed . slice ( firstBrace , lastBrace + 1 ) ) ;
125+ }
126+
127+ for ( const candidate of candidates ) {
128+ try {
129+ const parsed : unknown = JSON . parse ( candidate ) ;
130+ if ( isRecord ( parsed ) && isDocumentResultPayload ( parsed ) ) {
131+ return parsed ;
132+ }
133+ } catch {
134+ // Try the next candidate.
135+ }
136+ }
137+
138+ return undefined ;
139+ }
140+
141+ function isDocumentResultPayload ( payload : Record < string , unknown > ) : boolean {
142+ if ( typeof payload . fileId === 'string' || typeof payload . id === 'string' ) return true ;
143+ if ( isRecord ( payload . existingFile ) ) return true ;
144+ return payload . error === 'FILE_NOT_FOUND' || payload . error === 'FILE_EXISTS' ;
145+ }
146+
147+ function findDocumentPayload (
148+ value : unknown ,
149+ maxBytes : number ,
150+ depth = 0
151+ ) : Record < string , unknown > | undefined {
152+ if ( depth > 6 || value === null || value === undefined ) return undefined ;
153+
154+ if ( typeof value === 'string' ) {
155+ return parseDocumentPayloadCandidate ( value , maxBytes ) ;
156+ }
157+
158+ if ( Array . isArray ( value ) ) {
159+ for ( const entry of value ) {
160+ const found = findDocumentPayload ( entry , maxBytes , depth + 1 ) ;
161+ if ( found ) return found ;
162+ }
163+ return undefined ;
164+ }
165+
166+ if ( ! isRecord ( value ) ) return undefined ;
167+
168+ for ( const key of [ 'text' , 'output' , 'content' , 'result' , 'message' ] ) {
169+ const found = findDocumentPayload ( value [ key ] , maxBytes , depth + 1 ) ;
170+ if ( found ) return found ;
171+ }
172+
173+ return undefined ;
174+ }
175+
176+ function extractDocumentCardRawOutput (
177+ meta : Record < string , unknown > ,
178+ contentArray : unknown [ ] ,
179+ maxBytes : number
180+ ) : Array < { type : 'text' ; text : string } > | undefined {
181+ if ( ! isDocumentCardTool ( meta ) ) return undefined ;
182+ if ( meta . rawOutput !== undefined && meta . rawOutput !== null ) return undefined ;
183+
184+ const payload = findDocumentPayload ( contentArray , maxBytes ) ;
185+ if ( ! payload ) return undefined ;
186+
187+ const text = JSON . stringify ( payload ) ;
188+ if ( textEncoder . encode ( text ) . byteLength > maxBytes ) {
189+ return undefined ;
190+ }
191+
192+ return [ { type : 'text' , text } ] ;
193+ }
76194
77- export function stripToolMetadataContent ( meta : unknown ) : unknown {
195+ export function stripToolMetadataContent ( meta : unknown , options ?: CompactMessageOptions ) : unknown {
78196 if ( ! meta || typeof meta !== 'object' ) return meta ;
79197 const obj = expectJsonRecord ( meta , 'project-data.tool_metadata' ) ;
80198 const contentArray = obj . content ;
81199 if ( ! Array . isArray ( contentArray ) || contentArray . length === 0 ) return meta ;
82200
83201 const contentJson = JSON . stringify ( contentArray ) ;
84202 const contentSize = textEncoder . encode ( contentJson ) . byteLength ;
203+ const rawOutput = extractDocumentCardRawOutput (
204+ obj ,
205+ contentArray ,
206+ resolveDocumentCardRawOutputMaxBytes ( options )
207+ ) ;
85208
86209 const rest = Object . fromEntries ( Object . entries ( obj ) . filter ( ( [ k ] ) => k !== 'content' ) ) ;
87- return { ...rest , contentSize } ;
210+ return rawOutput ? { ... rest , rawOutput , contentSize } : { ...rest , contentSize } ;
88211}
89212
90213/** Search result row (message + session join) */
0 commit comments