@@ -2,14 +2,13 @@ import Supermemory from "supermemory";
22import { CONFIG , SUPERMEMORY_API_KEY , isConfigured } from "../config.js" ;
33import { log } from "./logger.js" ;
44import type {
5- MemoryType ,
6- ConversationMessage ,
75 ConversationIngestResponse ,
6+ ConversationMessage ,
7+ MemoryType ,
88} from "../types/index.js" ;
99
10- const SUPERMEMORY_API_URL = "https://api.supermemory.ai" ;
11-
1210const TIMEOUT_MS = 30000 ;
11+ const MAX_CONVERSATION_CHARS = 100_000 ;
1312
1413function withTimeout < T > ( promise : Promise < T > , ms : number ) : Promise < T > {
1514 return Promise . race ( [
@@ -23,6 +22,31 @@ function withTimeout<T>(promise: Promise<T>, ms: number): Promise<T> {
2322export class SupermemoryClient {
2423 private client : Supermemory | null = null ;
2524
25+ private formatConversationMessage ( message : ConversationMessage ) : string {
26+ const content =
27+ typeof message . content === "string"
28+ ? message . content
29+ : message . content
30+ . map ( ( part ) =>
31+ part . type === "text"
32+ ? part . text
33+ : `[image] ${ part . imageUrl . url } `
34+ )
35+ . join ( "\n" ) ;
36+
37+ const trimmed = content . trim ( ) ;
38+ if ( trimmed . length === 0 ) {
39+ return `[${ message . role } ]` ;
40+ }
41+ return `[${ message . role } ] ${ trimmed } ` ;
42+ }
43+
44+ private formatConversationTranscript ( messages : ConversationMessage [ ] ) : string {
45+ return messages
46+ . map ( ( message , idx ) => `${ idx + 1 } . ${ this . formatConversationMessage ( message ) } ` )
47+ . join ( "\n" ) ;
48+ }
49+
2650 private getClient ( ) : Supermemory {
2751 if ( ! this . client ) {
2852 if ( ! isConfigured ( ) ) {
@@ -145,40 +169,78 @@ export class SupermemoryClient {
145169 containerTags : string [ ] ,
146170 metadata ?: Record < string , string | number | boolean >
147171 ) {
148- log ( "ingestConversation: start" , { conversationId, messageCount : messages . length } ) ;
149- try {
150- const response = await withTimeout (
151- fetch ( `${ SUPERMEMORY_API_URL } /conversations` , {
152- method : "POST" ,
153- headers : {
154- "Content-Type" : "application/json" ,
155- Authorization : `Bearer ${ SUPERMEMORY_API_KEY } ` ,
156- } ,
157- body : JSON . stringify ( {
158- conversationId,
159- messages,
160- containerTags,
161- metadata,
162- } ) ,
163- } ) ,
164- TIMEOUT_MS
165- ) ;
172+ log ( "ingestConversation: start" , {
173+ conversationId,
174+ messageCount : messages . length ,
175+ containerTags,
176+ } ) ;
166177
167- if ( ! response . ok ) {
168- const errorText = await response . text ( ) ;
169- log ( "ingestConversation: error response" , { status : response . status , error : errorText } ) ;
170- return { success : false as const , error : `HTTP ${ response . status } : ${ errorText } ` } ;
178+ if ( messages . length === 0 ) {
179+ return { success : false as const , error : "No messages to ingest" } ;
180+ }
181+
182+ const uniqueTags = [ ...new Set ( containerTags ) ] . filter ( ( tag ) => tag . length > 0 ) ;
183+ if ( uniqueTags . length === 0 ) {
184+ return { success : false as const , error : "At least one containerTag is required" } ;
185+ }
186+
187+ const transcript = this . formatConversationTranscript ( messages ) ;
188+ const rawContent = `[Conversation ${ conversationId } ]\n${ transcript } ` ;
189+ const content =
190+ rawContent . length > MAX_CONVERSATION_CHARS
191+ ? `${ rawContent . slice ( 0 , MAX_CONVERSATION_CHARS ) } \n...[truncated]`
192+ : rawContent ;
193+
194+ const ingestMetadata = {
195+ type : "conversation" as const ,
196+ conversationId,
197+ messageCount : messages . length ,
198+ originalContainerTags : uniqueTags ,
199+ ...metadata ,
200+ } ;
201+
202+ const savedIds : string [ ] = [ ] ;
203+ let firstError : string | null = null ;
204+
205+ for ( const tag of uniqueTags ) {
206+ const result = await this . addMemory ( content , tag , ingestMetadata ) ;
207+ if ( result . success ) {
208+ savedIds . push ( result . id ) ;
209+ } else if ( ! firstError ) {
210+ firstError = result . error || "Failed to store conversation" ;
171211 }
212+ }
172213
173- const result = await response . json ( ) as ConversationIngestResponse ;
174- log ( "ingestConversation: success" , { conversationId, status : result . status } ) ;
175- return { success : true as const , ...result } ;
176- } catch ( error ) {
177- const errorMessage = error instanceof Error ? error . message : String ( error ) ;
178- log ( "ingestConversation: error" , { error : errorMessage } ) ;
179- return { success : false as const , error : errorMessage } ;
214+ if ( savedIds . length === 0 ) {
215+ log ( "ingestConversation: error" , { conversationId, error : firstError } ) ;
216+ return {
217+ success : false as const ,
218+ error : firstError || "Failed to ingest conversation" ,
219+ } ;
180220 }
221+
222+ const status =
223+ savedIds . length === uniqueTags . length ? "stored" : "partial" ;
224+ const response : ConversationIngestResponse = {
225+ id : savedIds [ 0 ] ! ,
226+ conversationId,
227+ status,
228+ } ;
229+
230+ log ( "ingestConversation: success" , {
231+ conversationId,
232+ status,
233+ storedCount : savedIds . length ,
234+ requestedCount : uniqueTags . length ,
235+ } ) ;
236+
237+ return {
238+ success : true as const ,
239+ ...response ,
240+ storedMemoryIds : savedIds ,
241+ } ;
181242 }
243+
182244}
183245
184246export const supermemoryClient = new SupermemoryClient ( ) ;
0 commit comments