Skip to content

Commit 7a20a56

Browse files
authored
Merge pull request #21 from slapglif/fix-ingest-conversation-memory-backend
fix: preserve ingestConversation by routing through memories API
2 parents e315fd3 + 4b93a2f commit 7a20a56

1 file changed

Lines changed: 95 additions & 33 deletions

File tree

src/services/client.ts

Lines changed: 95 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,13 @@ import Supermemory from "supermemory";
22
import { CONFIG, SUPERMEMORY_API_KEY, isConfigured } from "../config.js";
33
import { log } from "./logger.js";
44
import 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-
1210
const TIMEOUT_MS = 30000;
11+
const MAX_CONVERSATION_CHARS = 100_000;
1312

1413
function 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> {
2322
export 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

184246
export const supermemoryClient = new SupermemoryClient();

0 commit comments

Comments
 (0)