Skip to main content

Convex Integration

Info
Last Updated: 2026-01-08

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 memory
  • memories.search - Semantic or keyword search
  • memories.list - Paginated listing
  • memories.count - Count with filters
  • conversations.get - Get conversation
  • conversations.getHistory - Get message thread
  • immutable.get - Get immutable record
  • mutable.get - Get mutable value
  • contexts.get - Get context
  • users.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 memory
  • memories.update - Update memory (creates version)
  • memories.delete - Delete memory
  • conversations.create - Create conversation
  • conversations.addMessage - Append message
  • immutable.store - Store versioned record
  • mutable.set - Set mutable value
  • mutable.update - Atomic update
  • contexts.create - Create context
  • contexts.update - Update context status
  • users.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 integration
  • users.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


Questions? Ask in GitHub Discussions.