@cortexmemory/sdk
Version:
AI agent memory SDK built on Convex - ACID storage, vector search, and conversation management
460 lines (402 loc) • 11.6 kB
text/typescript
/**
* Cortex SDK - Memory Spaces Registry
*
* Hive/Collaboration Mode management
* Memory space metadata and analytics
*/
import { v } from "convex/values";
import { mutation, query } from "./_generated/server";
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// Mutations (Write Operations)
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
/**
* Register a new memory space
*/
export const register = mutation({
args: {
memorySpaceId: v.string(),
name: v.optional(v.string()),
type: v.union(
v.literal("personal"),
v.literal("team"),
v.literal("project"),
v.literal("custom"),
),
participants: v.array(
v.object({
id: v.string(),
type: v.string(), // "user", "agent", "tool", etc.
joinedAt: v.number(),
}),
),
metadata: v.optional(v.any()),
},
handler: async (ctx, args) => {
// Check if already exists
const existing = await ctx.db
.query("memorySpaces")
.withIndex("by_memorySpaceId", (q) =>
q.eq("memorySpaceId", args.memorySpaceId),
)
.first();
if (existing) {
throw new Error("MEMORYSPACE_ALREADY_EXISTS");
}
const now = Date.now();
const _id = await ctx.db.insert("memorySpaces", {
memorySpaceId: args.memorySpaceId,
name: args.name,
type: args.type,
participants: args.participants,
metadata: args.metadata || {},
status: "active",
createdAt: now,
updatedAt: now,
});
return await ctx.db.get(_id);
},
});
/**
* Update memory space metadata
*/
export const update = mutation({
args: {
memorySpaceId: v.string(),
name: v.optional(v.string()),
metadata: v.optional(v.any()),
status: v.optional(v.union(v.literal("active"), v.literal("archived"))),
},
handler: async (ctx, args) => {
const space = await ctx.db
.query("memorySpaces")
.withIndex("by_memorySpaceId", (q) =>
q.eq("memorySpaceId", args.memorySpaceId),
)
.first();
if (!space) {
throw new Error("MEMORYSPACE_NOT_FOUND");
}
await ctx.db.patch(space._id, {
name: args.name !== undefined ? args.name : space.name,
metadata: args.metadata !== undefined ? args.metadata : space.metadata,
status: args.status !== undefined ? args.status : space.status,
updatedAt: Date.now(),
});
return await ctx.db.get(space._id);
},
});
/**
* Add participant to memory space
*/
export const addParticipant = mutation({
args: {
memorySpaceId: v.string(),
participant: v.object({
id: v.string(),
type: v.string(),
joinedAt: v.number(),
}),
},
handler: async (ctx, args) => {
const space = await ctx.db
.query("memorySpaces")
.withIndex("by_memorySpaceId", (q) =>
q.eq("memorySpaceId", args.memorySpaceId),
)
.first();
if (!space) {
throw new Error("MEMORYSPACE_NOT_FOUND");
}
// Check if already exists
if (space.participants.some((p) => p.id === args.participant.id)) {
throw new Error("PARTICIPANT_ALREADY_EXISTS");
}
await ctx.db.patch(space._id, {
participants: [...space.participants, args.participant],
updatedAt: Date.now(),
});
return await ctx.db.get(space._id);
},
});
/**
* Remove participant from memory space
*/
export const removeParticipant = mutation({
args: {
memorySpaceId: v.string(),
participantId: v.string(),
},
handler: async (ctx, args) => {
const space = await ctx.db
.query("memorySpaces")
.withIndex("by_memorySpaceId", (q) =>
q.eq("memorySpaceId", args.memorySpaceId),
)
.first();
if (!space) {
throw new Error("MEMORYSPACE_NOT_FOUND");
}
const updatedParticipants = space.participants.filter(
(p) => p.id !== args.participantId,
);
await ctx.db.patch(space._id, {
participants: updatedParticipants,
updatedAt: Date.now(),
});
return await ctx.db.get(space._id);
},
});
/**
* Delete memory space (also cascades to all data)
*/
export const deleteSpace = mutation({
args: {
memorySpaceId: v.string(),
cascade: v.boolean(), // If true, delete all associated data
},
handler: async (ctx, args) => {
const space = await ctx.db
.query("memorySpaces")
.withIndex("by_memorySpaceId", (q) =>
q.eq("memorySpaceId", args.memorySpaceId),
)
.first();
if (!space) {
throw new Error("MEMORYSPACE_NOT_FOUND");
}
if (args.cascade) {
// Delete all conversations
const conversations = await ctx.db
.query("conversations")
.withIndex("by_memorySpace", (q) =>
q.eq("memorySpaceId", args.memorySpaceId),
)
.collect();
for (const conv of conversations) {
await ctx.db.delete(conv._id);
}
// Delete all memories
const memories = await ctx.db
.query("memories")
.withIndex("by_memorySpace", (q) =>
q.eq("memorySpaceId", args.memorySpaceId),
)
.collect();
for (const mem of memories) {
await ctx.db.delete(mem._id);
}
// Delete all facts
const facts = await ctx.db
.query("facts")
.withIndex("by_memorySpace", (q) =>
q.eq("memorySpaceId", args.memorySpaceId),
)
.collect();
for (const fact of facts) {
await ctx.db.delete(fact._id);
}
}
// Delete space itself
await ctx.db.delete(space._id);
return {
deleted: true,
memorySpaceId: args.memorySpaceId,
cascaded: args.cascade,
};
},
});
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// Queries (Read Operations)
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
/**
* Get memory space by ID
*/
export const get = query({
args: {
memorySpaceId: v.string(),
},
handler: async (ctx, args) => {
const space = await ctx.db
.query("memorySpaces")
.withIndex("by_memorySpaceId", (q) =>
q.eq("memorySpaceId", args.memorySpaceId),
)
.first();
return space || null;
},
});
/**
* List memory spaces
*/
export const list = query({
args: {
type: v.optional(
v.union(
v.literal("personal"),
v.literal("team"),
v.literal("project"),
v.literal("custom"),
),
),
status: v.optional(v.union(v.literal("active"), v.literal("archived"))),
limit: v.optional(v.number()),
},
handler: async (ctx, args) => {
let spaces = await ctx.db
.query("memorySpaces")
.order("desc")
.take(args.limit || 100);
// Apply filters
if (args.type) {
spaces = spaces.filter((s) => s.type === args.type);
}
if (args.status) {
spaces = spaces.filter((s) => s.status === args.status);
}
return spaces;
},
});
/**
* Count memory spaces
*/
export const count = query({
args: {
type: v.optional(
v.union(
v.literal("personal"),
v.literal("team"),
v.literal("project"),
v.literal("custom"),
),
),
status: v.optional(v.union(v.literal("active"), v.literal("archived"))),
},
handler: async (ctx, args) => {
let spaces = await ctx.db.query("memorySpaces").collect();
if (args.type) {
spaces = spaces.filter((s) => s.type === args.type);
}
if (args.status) {
spaces = spaces.filter((s) => s.status === args.status);
}
return spaces.length;
},
});
/**
* Get memory space statistics
*/
export const getStats = query({
args: {
memorySpaceId: v.string(),
},
handler: async (ctx, args) => {
const space = await ctx.db
.query("memorySpaces")
.withIndex("by_memorySpaceId", (q) =>
q.eq("memorySpaceId", args.memorySpaceId),
)
.first();
if (!space) {
throw new Error("MEMORYSPACE_NOT_FOUND");
}
// Count conversations
const conversationCount = await ctx.db
.query("conversations")
.withIndex("by_memorySpace", (q) =>
q.eq("memorySpaceId", args.memorySpaceId),
)
.collect()
.then((c) => c.length);
// Count memories
const memoryCount = await ctx.db
.query("memories")
.withIndex("by_memorySpace", (q) =>
q.eq("memorySpaceId", args.memorySpaceId),
)
.collect()
.then((m) => m.length);
// Count facts
const factCount = await ctx.db
.query("facts")
.withIndex("by_memorySpace", (q) =>
q.eq("memorySpaceId", args.memorySpaceId),
)
.collect()
.then((f) => f.filter((fact) => fact.supersededBy === undefined).length); // Active facts only
// Calculate total messages
const conversations = await ctx.db
.query("conversations")
.withIndex("by_memorySpace", (q) =>
q.eq("memorySpaceId", args.memorySpaceId),
)
.collect();
const messageCount = conversations.reduce(
(sum, conv) => sum + conv.messageCount,
0,
);
return {
memorySpaceId: args.memorySpaceId,
totalMemories: memoryCount,
totalConversations: conversationCount,
totalFacts: factCount,
totalMessages: messageCount,
storage: {
conversationsBytes: 0, // TODO: Implement size calculation
memoriesBytes: 0,
factsBytes: 0,
totalBytes: 0,
},
topTags: [], // TODO: Implement tag aggregation
importanceBreakdown: {
critical: 0,
high: 0,
medium: 0,
low: 0,
trivial: 0,
},
};
},
});
/**
* Find memory spaces by participant
*/
export const findByParticipant = query({
args: {
participantId: v.string(),
},
handler: async (ctx, args) => {
const allSpaces = await ctx.db.query("memorySpaces").collect();
return allSpaces.filter((space) =>
space.participants.some((p) => p.id === args.participantId),
);
},
});
/**
* Purge all memory spaces (TEST/DEV ONLY)
*/
export const purgeAll = mutation({
args: {},
handler: async (ctx) => {
// Safety check: Only allow in test/dev environments
const siteUrl = process.env.CONVEX_SITE_URL || "";
const isLocal =
siteUrl.includes("localhost") || siteUrl.includes("127.0.0.1");
const isDevDeployment =
siteUrl.includes(".convex.site") ||
siteUrl.includes("dev-") ||
siteUrl.includes("convex.cloud");
const isTestEnv =
process.env.NODE_ENV === "test" ||
process.env.CONVEX_ENVIRONMENT === "test";
if (!isLocal && !isDevDeployment && !isTestEnv) {
throw new Error(
"PURGE_DISABLED_IN_PRODUCTION: purgeAll is only available in test/dev environments.",
);
}
const allSpaces = await ctx.db.query("memorySpaces").collect();
for (const space of allSpaces) {
await ctx.db.delete(space._id);
}
return { deleted: allSpaces.length };
},
});