Convex Integration
How Cortex leverages Convex features for persistent memory, vector search, and real-time updates.
Overview
Cortex is built natively on Convex - not as a wrapper, but as a first-class Convex application. We use Convex's features directly:
- Queries - Read operations (reactive, cacheable)
- Mutations - Write operations (ACID transactions)
- Actions - External calls (embeddings, pub/sub)
- Vector Search - Native semantic similarity
- Search Indexes - Full-text keyword search
- Real-time - Reactive query subscriptions
- TypeScript - End-to-end type safety
Convex Function Types
Queries (Read Operations)
Queries are reactive, cached, and deterministic:
// convex/memories.ts
import { query } from "./_generated/server";
import { v } from "convex/values";
export const get = query({
args: { memorySpaceId: v.string(), memoryId: v.id("memories") },
handler: async (ctx, args) => {
const memory = await ctx.db.get(args.memoryId);
// Verify memory space owns this memory
if (!memory || memory.memorySpaceId !== args.memorySpaceId) {
return null;
}
return memory;
},
});
export const search = query({
args: {
memorySpaceId: v.string(),
query: v.string(),
embedding: v.optional(v.array(v.float64())),
filters: v.any(),
},
handler: async (ctx, args) => {
let results;
if (args.embedding) {
// Semantic search
results = await ctx.db
.query("memories")
.withIndex("by_embedding", (q) =>
q
.similar("embedding", args.embedding, args.filters.limit || 20)
.eq("memorySpaceId", args.memorySpaceId),
)
.collect();
} else {
// Keyword search
results = await ctx.db
.query("memories")
.withSearchIndex("by_content", (q) =>
q
.search("content", args.query)
.eq("memorySpaceId", args.memorySpaceId),
)
.collect();
}
// Apply filters (importance, tags, dates, participantId, etc.)
return applyFilters(results, args.filters);
},
});
Characteristics:
- Read-only (cannot modify database)
- Reactive (auto-update on data changes)
- Cacheable (Convex caches results)
- Fast (optimized by Convex)
Mutations (Write Operations)
Mutations modify data with ACID guarantees:
// convex/memories.ts
import { mutation } from "./_generated/server";
import { v } from "convex/values";
export const store = mutation({
args: {
memorySpaceId: v.string(),
content: v.string(),
contentType: v.union(v.literal("raw"), v.literal("summarized")),
embedding: v.optional(v.array(v.float64())),
source: v.any(),
conversationRef: v.optional(v.any()),
metadata: v.any(),
},
handler: async (ctx, args) => {
// Insert into memories table
const memoryId = await ctx.db.insert("memories", {
...args,
version: 1,
previousVersions: [],
accessCount: 0,
createdAt: Date.now(),
updatedAt: Date.now(),
});
return await ctx.db.get(memoryId);
},
});
export const update = mutation({
args: {
memorySpaceId: v.string(),
memoryId: v.id("memories"),
updates: v.any(),
},
handler: async (ctx, args) => {
const memory = await ctx.db.get(args.memoryId);
if (!memory || memory.memorySpaceId !== args.memorySpaceId) {
throw new ConvexError("MEMORY_NOT_FOUND");
}
// Create new version (preserve old)
const newVersion = memory.version + 1;
const previousVersions = [
...memory.previousVersions,
{
version: memory.version,
content: memory.content,
metadata: memory.metadata,
timestamp: memory.updatedAt,
},
];
// Apply retention (keep last 10 versions)
const retention = 10;
if (previousVersions.length > retention) {
previousVersions.shift(); // Remove oldest
}
// Update document
await ctx.db.patch(args.memoryId, {
...args.updates,
version: newVersion,
previousVersions,
updatedAt: Date.now(),
});
return await ctx.db.get(args.memoryId);
},
});
Characteristics:
- Atomic (all-or-nothing)
- Consistent (sees latest data)
- Isolated (no race conditions)
- Durable (persisted immediately)
Actions (External Calls)
Actions can call external APIs (embeddings, pub/sub):
// convex/actions.ts
import { action } from "./_generated/server";
import { v } from "convex/values";
export const storeWithEmbedding = action({
args: {
memorySpaceId: v.string(),
content: v.string(),
metadata: v.any(),
},
handler: async (ctx, args) => {
// 1. Call external embedding API
const response = await fetch("https://api.openai.com/v1/embeddings", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.OPENAI_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
model: "text-embedding-3-large",
input: args.content,
}),
});
const data = await response.json();
const embedding = data.data[0].embedding;
// 2. Store in database via mutation
return await ctx.runMutation("memories:store", {
...args,
embedding,
contentType: "raw",
source: { type: "system", timestamp: Date.now() },
});
},
});
export const publishA2ANotification = action({
args: {
memorySpaceId: v.string(),
notification: v.any(),
},
handler: async (ctx, args) => {
// Call external pub/sub (Redis, RabbitMQ, etc.)
const redis = await connectRedis(process.env.REDIS_URL);
await redis.publish(
`memorySpace:${args.memorySpaceId}:inbox`,
JSON.stringify(args.notification),
);
await redis.quit();
},
});
Characteristics:
- Can call external APIs
- Can call mutations/queries
- Non-deterministic allowed
- Used for embeddings, pub/sub, webhooks
How Cortex Uses Each Type
Queries (Read Path)
All read operations use Convex queries:
// SDK call
const memory = await cortex.memory.get("agent-1", "mem_abc");
// Becomes Convex query
await client.query(api.memories.get, {
memorySpaceId: "agent-1",
memoryId: "mem_abc",
});
// Reactive in UI
const { data: memory } = useQuery(api.memories.get, {
memorySpaceId: "agent-1",
memoryId: "mem_abc",
});
// ↑ Auto-updates when memory changes!
Cortex queries:
memories.get- Get single memorymemories.search- Semantic or keyword searchmemories.list- Paginated listingmemories.count- Count with filtersconversations.get- Get conversationconversations.getHistory- Get message threadimmutable.get- Get immutable recordmutable.get- Get mutable valuecontexts.get- Get contextusers.get- Get user profile (via immutable)
Mutations (Write Path)
All write operations use Convex mutations:
// SDK call
await cortex.memory.store("agent-1", data);
// Becomes Convex mutation
await client.mutation(api.memories.store, {
memorySpaceId: "agent-1",
...data,
});
Cortex mutations:
memories.store- Create memorymemories.update- Update memory (creates version)memories.delete- Delete memoryconversations.create- Create conversationconversations.addMessage- Append messageimmutable.store- Store versioned recordmutable.set- Set mutable valuemutable.update- Atomic updatecontexts.create- Create contextcontexts.update- Update context statususers.update- Update user profile (via immutable)
Actions (External Integration)
Actions handle non-deterministic operations:
// Direct Mode: Developer calls embedding API
const embedding = await openai.embeddings.create({ ... });
await cortex.memory.store('agent-1', { content, embedding });
// Cloud Mode (planned): Cortex action calls embedding API
await cortex.memory.store('agent-1', {
content,
autoEmbed: true, // ← Triggers action
});
// Convex action (Cloud Mode backend - planned)
export const storeWithAutoEmbed = action({
args: { memorySpaceId: v.string(), content: v.string(), ... },
handler: async (ctx, args) => {
// Call OpenAI
const embedding = await generateEmbedding(args.content);
// Store via mutation
return await ctx.runMutation("memories:store", {
...args,
embedding,
});
},
});
Cortex actions:
memories.storeWithAutoEmbed- Cloud Mode auto-embeddings (planned)a2a.publishNotification- Pub/sub integrationusers.cascadeDelete- Cloud Mode GDPR cascade (planned)governance.enforceRetention- Cleanup jobs
Vector Search Implementation
Vector Index Definition
// convex/schema.ts
memories: defineTable({
memoryId: v.string(),
memorySpaceId: v.string(),
participantId: v.optional(v.string()),
tenantId: v.optional(v.string()),
content: v.string(),
embedding: v.optional(v.array(v.float64())),
// ...
}).vectorIndex("by_embedding", {
vectorField: "embedding",
dimensions: 1536, // Default: text-embedding-3-small
filterFields: [
"memorySpaceId",
"tenantId",
"userId",
"agentId",
"participantId",
],
});
Vector Search Query
// convex/memories.ts
export const semanticSearch = query({
args: {
memorySpaceId: v.string(),
embedding: v.array(v.float64()),
tenantId: v.optional(v.string()),
userId: v.optional(v.string()),
participantId: v.optional(v.string()),
limit: v.number(),
},
handler: async (ctx, args) => {
let q = ctx.db
.query("memories")
.withIndex("by_embedding", (q) =>
q
.similar("embedding", args.embedding, args.limit)
.eq("memorySpaceId", args.memorySpaceId),
);
// Optional filters (pre-filtered before similarity via filterFields)
if (args.tenantId) {
q = q.filter((q) => q.eq(q.field("tenantId"), args.tenantId));
}
if (args.userId) {
q = q.filter((q) => q.eq(q.field("userId"), args.userId));
}
if (args.participantId) {
q = q.filter((q) => q.eq(q.field("participantId"), args.participantId));
}
return await q.collect();
},
});
Convex vector search features:
- Cosine similarity (built-in)
- Pre-filtering before similarity (filterFields)
- Multiple dimensions supported (1536, 3072, etc.)
- Sub-100ms for millions of vectors
Full-Text Search Implementation
Search Index Definition
// convex/schema.ts
memories: defineTable({
memorySpaceId: v.string(),
tenantId: v.optional(v.string()),
content: v.string(),
sourceType: v.union(...),
userId: v.optional(v.string()),
agentId: v.optional(v.string()),
participantId: v.optional(v.string()),
// ...
}).searchIndex("by_content", {
searchField: "content",
filterFields: ["memorySpaceId", "tenantId", "sourceType", "userId", "agentId", "participantId"],
});
Keyword Search Query
// convex/memories.ts
export const keywordSearch = query({
args: {
memorySpaceId: v.string(),
keywords: v.string(),
tenantId: v.optional(v.string()),
userId: v.optional(v.string()),
participantId: v.optional(v.string()),
limit: v.number(),
},
handler: async (ctx, args) => {
let results = await ctx.db
.query("memories")
.withSearchIndex("by_content", (q) => {
let search = q
.search("content", args.keywords)
.eq("memorySpaceId", args.memorySpaceId);
if (args.tenantId) {
search = search.eq("tenantId", args.tenantId);
}
return search;
})
.take(args.limit);
// Additional filtering
if (args.userId) {
results = results.filter((m) => m.userId === args.userId);
}
if (args.participantId) {
results = results.filter((m) => m.participantId === args.participantId);
}
return results;
},
});
Convex search features:
- Tokenization (automatic)
- Ranking by relevance
- Prefix matching
- Fast filtering before search
ACID Transactions
Single-Document Operations
// Automatic transaction for single doc
export const addMessage = mutation({
args: { conversationId: v.id("conversations"), message: v.any() },
handler: async (ctx, args) => {
const conversation = await ctx.db.get(args.conversationId);
// Atomic update (read + modify + write)
await ctx.db.patch(args.conversationId, {
messages: [...conversation.messages, args.message],
messageCount: conversation.messageCount + 1,
updatedAt: Date.now(),
});
},
});
Multi-Document Operations
// All mutations are transactional
export const remember = mutation({
args: { memorySpaceId: v.string(), conversationId: v.id("conversations"), userMessage: v.string(), agentResponse: v.string() },
handler: async (ctx, args) => {
// Step 1: Add to conversation (ACID)
const userMsgId = await ctx.db.insert("messages", { ... });
const agentMsgId = await ctx.db.insert("messages", { ... });
// Step 2: Create vector memory (ACID)
const memoryId = await ctx.db.insert("memories", {
conversationRef: {
conversationId: args.conversationId,
messageIds: [userMsgId, agentMsgId],
},
...
});
// If any step fails, ALL steps roll back
return { userMsgId, agentMsgId, memoryId };
},
});
Mutable Store Transactions
// Optimistic locking for mutable updates
export const atomicUpdate = mutation({
args: { namespace: v.string(), key: v.string(), updater: v.string() },
handler: async (ctx, args) => {
// Get current value
const record = await ctx.db
.query("mutable")
.withIndex("by_namespace_key", (q) =>
q.eq("namespace", args.namespace).eq("key", args.key),
)
.unique();
// Apply update operation (using union of operation types)
let newValue = record.value;
if (args.operation === "increment") {
newValue = record.value + args.amount;
} else if (args.operation === "decrement") {
newValue = record.value - args.amount;
} else if (args.operation === "append") {
newValue = [...record.value, ...args.items];
}
// Additional operations handled via operation union type
// Atomic update
await ctx.db.patch(record._id, {
value: newValue,
updatedAt: Date.now(),
});
// No race conditions - Convex handles concurrency
},
});
Convex ACID benefits:
- All mutations are transactions
- No manual locking needed
- Optimistic concurrency control
- Isolation levels automatic
Reactive Queries
Client-Side Reactivity
// React component
import { useQuery, useMutation } from "convex/react";
import { api } from "../convex/_generated/api";
function AgentMemoryList({ memorySpaceId }) {
// Reactive query - auto-updates!
const memories = useQuery(api.memories.list, {
memorySpaceId,
filters: { minImportance: 50 },
});
const storeMemory = useMutation(api.memories.store);
return (
<div>
{memories?.map(m => (
<div key={m._id}>{m.content}</div>
))}
<button onClick={() => storeMemory({ memorySpaceId, content: "New memory" })}>
Add Memory
</button>
</div>
);
// ↑ List auto-updates when mutation runs!
}
Server-Side Subscriptions
// Node.js server
import { ConvexClient } from "convex/browser";
const client = new ConvexClient(process.env.CONVEX_URL);
// Subscribe to query results
client.onUpdate(api.memories.list, { memorySpaceId: "agent-1" }, (memories) => {
console.log(`Agent now has ${memories.length} memories`);
// Called every time data changes!
});
Cortex usage:
- Real-time dashboards (Cloud Mode - planned)
- Live collaboration features
- Agent activity monitoring
Compound Operations (Layer 3)
remember() - SDK Orchestration Method
The remember() helper is an SDK method that orchestrates multiple Convex mutations across all layers. It's not a single Convex mutation, but rather a convenience method that coordinates multiple backend calls:
// SDK call
await cortex.memory.remember({
memorySpaceId: 'user-123-personal',
participantId: 'my-assistant', // NEW: Hive Mode tracking
conversationId: 'conv-123',
userMessage: 'Hello',
agentResponse: 'Hi!',
userId: 'user-123',
userName: 'Alex',
extractFacts: true, // NEW: Auto-extract facts
});
// SDK implementation orchestrates multiple Convex calls:
// 1. Call conversations.addMessage mutation (Layer 1a)
const userMsgId = await client.mutation(api.conversations.addMessage, {
conversationId: args.conversationId,
message: {
id: generateId(),
role: "user",
content: args.userMessage,
userId: args.userId,
participantId: args.participantId,
timestamp: Date.now(),
},
});
// 2. Call conversations.addMessage mutation again (Layer 1a)
const agentMsgId = await client.mutation(api.conversations.addMessage, {
conversationId: args.conversationId,
message: {
id: generateId(),
role: "agent",
content: args.agentResponse,
participantId: args.participantId,
timestamp: Date.now(),
},
});
// 3. Call memories.store mutation (Layer 2)
const memoryId = await client.mutation(api.memories.store, {
memorySpaceId: args.memorySpaceId,
participantId: args.participantId,
tenantId: args.tenantId,
userId: args.userId,
content: `${args.userName}: ${args.userMessage}\nAgent: ${args.agentResponse}`,
contentType: "summarized",
sourceType: "conversation",
sourceUserId: args.userId,
sourceUserName: args.userName,
sourceTimestamp: Date.now(),
messageRole: "user",
conversationRef: {
conversationId: args.conversationId,
messageIds: [userMsgId, agentMsgId], // ← Links to Layer 1a
},
importance: args.importance || 50,
tags: args.tags || [],
});
// 4. Optionally extract and store facts (Layer 3) if configured
let factIds = [];
if (args.extractFacts) {
const facts = await extractFactsFromExchange(
args.userMessage,
args.agentResponse
);
for (const fact of facts) {
const factId = await client.mutation(api.facts.store, {
...fact,
memorySpaceId: args.memorySpaceId,
participantId: args.participantId,
userId: args.userId,
sourceRef: {
conversationId: args.conversationId,
messageIds: [userMsgId, agentMsgId],
},
});
factIds.push(factId);
}
}
// Return combined result
return {
conversation: { messageIds: [userMsgId, agentMsgId] },
memories: [memoryId],
facts: factIds, // NEW: Facts extracted
};
Transaction guarantee: All inserts succeed or all fail (no partial state).
Orchestration: Handles L1a + L2 + L3 in one call.
Index Usage Patterns
Memory Space Isolation
// Compound index for space+user queries
.index("by_memorySpace_userId", ["memorySpaceId", "userId"])
// Efficient query
const memories = await ctx.db
.query("memories")
.withIndex("by_memorySpace_userId", (q) =>
q.eq("memorySpaceId", memorySpaceId).eq("userId", userId)
)
.collect();
// Uses compound index - O(log n)
Multi-Tenancy
// Tenant+space compound index
.index("by_tenant_space", ["tenantId", "memorySpaceId"])
// Tenant-isolated query
const memories = await ctx.db
.query("memories")
.withIndex("by_tenant_space", (q) =>
q.eq("tenantId", tenantId).eq("memorySpaceId", memorySpaceId)
)
.collect();
GDPR Cascade
// userId index across all tables
.index("by_userId", ["userId"])
// Fast cascade query (across ALL memory spaces)
const allMemories = await ctx.db
.query("memories")
.withIndex("by_userId", (q) => q.eq("userId", "user-123"))
.collect();
const allFacts = await ctx.db
.query("facts")
.withIndex("by_userId", (q) => q.eq("userId", "user-123"))
.collect();
// Delete all (in transaction)
for (const record of [...allMemories, ...allFacts]) {
await ctx.db.delete(record._id);
}
Hierarchical Contexts (Cross-Space Support)
// Multiple indexes for hierarchy navigation
.index("by_parentId", ["parentId"])
.index("by_rootId", ["rootId"])
.index("by_memorySpace", ["memorySpaceId"])
.index("by_memorySpace_status", ["memorySpaceId", "status"])
// Get all children (can be cross-space)
await ctx.db
.query("contexts")
.withIndex("by_parentId", (q) => q.eq("parentId", parentId))
.collect();
// Get entire workflow
await ctx.db
.query("contexts")
.withIndex("by_rootId", (q) => q.eq("rootId", rootId))
.collect();
// Get active contexts in memory space
await ctx.db
.query("contexts")
.withIndex("by_memorySpace_status", (q) =>
q.eq("memorySpaceId", memorySpaceId).eq("status", "active")
)
.collect();
Versioning Implementation
Automatic Version Management
export const update = mutation({
handler: async (ctx, args) => {
const current = await ctx.db.get(args.id);
// Create version snapshot
const snapshot = {
version: current.version,
content: current.content,
metadata: current.metadata,
timestamp: current.updatedAt,
};
// Add to history (with retention)
const previousVersions = [...current.previousVersions, snapshot];
// Apply retention limit
const retention = args.retention || 10;
while (previousVersions.length > retention) {
previousVersions.shift(); // Remove oldest
}
// Update with new version
await ctx.db.patch(args.id, {
content: args.newContent,
version: current.version + 1,
previousVersions,
updatedAt: Date.now(),
});
},
});
Stored in same document - no separate versions table needed.
Real-Time Features
Live Dashboard Example
// React component with real-time updates
function AgentDashboard({ memorySpaceId }) {
// Query auto-updates when data changes
const stats = useQuery(api.memorySpaces.getStats, { memorySpaceId });
const recentMemories = useQuery(api.memories.list, {
memorySpaceId,
limit: 10,
});
const activeContexts = useQuery(api.contexts.list, {
memorySpaceId,
status: "active",
});
return (
<div>
<h2>{stats?.name}</h2>
<p>{stats?.totalMemories} memories</p>
<p>{recentMemories?.length} recent</p>
<p>{activeContexts?.length} active workflows</p>
{/* All values update in real-time! */}
</div>
);
}
Subscription-Based Triggers
// Server-side agent runner
const client = new ConvexClient(process.env.CONVEX_URL);
// Watch for new A2A messages
client.onUpdate(
api.memories.list,
{
memorySpaceId: "hr-agent",
filters: {
"source.type": "a2a",
"metadata.direction": "inbound",
"metadata.responded": false,
},
},
async (pendingRequests) => {
for (const request of pendingRequests) {
// Process request
const answer = await processRequest(request.content);
// Respond
await client.mutation(api.a2a.send, {
fromMemorySpace: "hr-agent",
toMemorySpace: request.source.fromMemorySpace,
message: answer,
});
}
},
);
Performance Optimizations
Pagination
export const list = query({
args: {
memorySpaceId: v.string(),
limit: v.number(),
offset: v.number(),
},
handler: async (ctx, args) => {
const memories = await ctx.db
.query("memories")
.withIndex("by_memorySpace", (q) => q.eq("memorySpaceId", args.memorySpaceId))
.order("desc") // Most recent first
.skip(args.offset)
.take(args.limit)
.collect();
const total = await ctx.db
.query("memories")
.withIndex("by_memorySpace", (q) => q.eq("memorySpaceId", args.memorySpaceId))
.count();
return {
memories,
total,
hasMore: args.offset + args.limit < total,
};
},
});
Lazy Loading
// Get conversation without messages initially
export const getConversationMeta = query({
args: { conversationId: v.id("conversations") },
handler: async (ctx, args) => {
const conversation = await ctx.db.get(args.conversationId);
// Return metadata only (no messages)
// Note: lastMessageAt is computed from messages array, not stored
const lastMessageAt = conversation.messages.length > 0
? conversation.messages[conversation.messages.length - 1].timestamp
: conversation.createdAt;
return {
conversationId: conversation._id,
type: conversation.type,
participants: conversation.participants,
messageCount: conversation.messageCount,
lastMessageAt,
};
},
});
// Load messages separately when needed
export const getMessages = query({
args: {
conversationId: v.id("conversations"),
limit: v.number(),
offset: v.number(),
},
handler: async (ctx, args) => {
const conversation = await ctx.db.get(args.conversationId);
// Paginate messages
return conversation.messages.slice(args.offset, args.offset + args.limit);
},
});
Filter Before Sort
// Efficient: Use index, then filter, then sort
const memories = await ctx.db
.query("memories")
.withIndex("by_memorySpace", (q) => q.eq("memorySpaceId", memorySpaceId))
.filter((q) => q.gte(q.field("metadata.importance"), 70))
.order("desc") // Sort by createdAt
.take(20)
.collect();
// Inefficient: Filter without index
const all = await ctx.db.query("memories").collect();
const filtered = all.filter(
(m) => m.memorySpaceId === memorySpaceId && m.metadata.importance >= 70,
);
TypeScript Type Safety
Generated API Types
// Convex generates types from schema
import { api } from "./_generated/api";
import { Id, Doc } from "./_generated/dataModel";
// Type-safe function calls
const memory: Doc<"memories"> = await client.query(api.memories.get, {
memorySpaceId: "agent-1",
memoryId: "mem_abc" as Id<"memories">,
});
// Type errors caught at compile-time
await client.query(api.memories.get, {
memorySpaceId: 123, // TypeScript error: number not assignable to string
memoryId: "mem_abc",
});
SDK Type Wrappers
// Cortex SDK wraps Convex types
import { MemoryEntry } from "@cortex-platform/sdk";
// Convex Doc<"memories"> → Cortex MemoryEntry
class CortexSDK {
async get(
memorySpaceId: string,
memoryId: string,
): Promise<MemoryEntry | null> {
const doc = await this.client.query(api.memories.get, {
memorySpaceId,
memoryId: memoryId as Id<"memories">,
});
// Transform Convex doc to SDK type
return doc ? this.toMemoryEntry(doc) : null;
}
private toMemoryEntry(doc: Doc<"memories">): MemoryEntry {
return {
id: doc._id,
memorySpaceId: doc.memorySpaceId,
userId: doc.userId,
content: doc.content,
contentType: doc.contentType,
embedding: doc.embedding,
source: doc.source,
conversationRef: doc.conversationRef,
metadata: doc.metadata,
createdAt: new Date(doc.createdAt),
updatedAt: new Date(doc.updatedAt),
lastAccessed: doc.lastAccessed ? new Date(doc.lastAccessed) : undefined,
accessCount: doc.accessCount,
version: doc.version,
previousVersions: doc.previousVersions,
};
}
}
Background Tasks
Retention Cleanup
// Cron job for governance
export const cleanupOldVersions = mutation({
args: {},
handler: async (ctx) => {
const memories = await ctx.db.query("memories").collect();
for (const memory of memories) {
if (memory.previousVersions.length > 10) {
// Trim to retention limit
await ctx.db.patch(memory._id, {
previousVersions: memory.previousVersions.slice(-10),
});
}
}
},
});
// Schedule daily
export default {
cleanupVersions: {
schedule: "0 2 * * *", // 2 AM daily
handler: api.governance.cleanupOldVersions,
},
};
Error Handling
Convex Error Patterns
export const store = mutation({
handler: async (ctx, args) => {
// Validation
if (!args.content || args.content.length === 0) {
throw new ConvexError("INVALID_CONTENT: Content cannot be empty");
}
if (args.metadata.importance < 0 || args.metadata.importance > 100) {
throw new ConvexError("INVALID_IMPORTANCE: Importance must be 0-100");
}
// Permission check
const memorySpace = await ctx.db
.query("memorySpaces")
.withIndex("by_memorySpaceId", (q) => q.eq("memorySpaceId", args.memorySpaceId))
.first();
if (!memorySpace) {
throw new ConvexError("INVALID_MEMORY_SPACE_ID: Memory space not found");
}
// Insert
try {
return await ctx.db.insert("memories", args);
} catch (error) {
throw new ConvexError(`CONVEX_ERROR: Database insert failed - ${error}`);
}
},
});
SDK Error Transformation
// Cortex SDK catches Convex errors
try {
await cortex.memory.store("agent-1", data);
} catch (error) {
// Transform Convex error to Cortex error
if (error.data?.code) {
throw new CortexError(
error.data.code,
error.data.message,
error.data.details,
);
}
throw error;
}
Deployment Patterns
Direct Mode (Your Convex Instance)
// Your Convex backend
// convex/memories.ts - Your functions
export const store = mutation({ ... });
export const search = query({ ... });
// Your app
import { ConvexClient } from "convex/browser";
import { Cortex } from "@cortex-platform/sdk";
const convexClient = new ConvexClient(process.env.CONVEX_URL);
// Cortex SDK uses your Convex client
const cortex = new Cortex({
convexUrl: process.env.CONVEX_URL,
// OR provide client directly
client: convexClient,
});
Cloud Mode (Cortex-Managed Functions) - Planned
// Your Convex instance (just storage)
// No custom functions needed - Cortex Cloud provides them
// Your app (planned)
const cortex = new Cortex({
mode: "cloud",
apiKey: process.env.CORTEX_CLOUD_KEY,
});
// Cortex Cloud API (planned):
// - Deploys functions to your Convex
// - OR uses Cortex-hosted functions
// - Manages credentials securely
Next Steps
- Vector Embeddings - Embedding strategy and dimensions
- Search Strategy - Multi-strategy search implementation
- Performance - Optimization techniques
- Security & Privacy - Data protection
Questions? Ask in GitHub Discussions.