feat(Phase 4): Memory & Intelligence — memory, log, summarization, skills (#91)

Co-authored-by: Jason Woltje <jason@diversecanvas.com>
Co-committed-by: Jason Woltje <jason@diversecanvas.com>
This commit was merged in pull request #91.
This commit is contained in:
2026-03-13 13:56:50 +00:00
committed by jason.woltje
parent d83ebe65e9
commit 9eb48e1d9b
35 changed files with 1481 additions and 16 deletions

View File

@@ -0,0 +1,89 @@
import { eq, and, desc, sql, lt, type Db, insights } from '@mosaic/db';
export type Insight = typeof insights.$inferSelect;
export type NewInsight = typeof insights.$inferInsert;
export interface SearchResult {
insight: Insight;
distance: number;
}
export function createInsightsRepo(db: Db) {
return {
async findByUser(userId: string, limit = 50): Promise<Insight[]> {
return db
.select()
.from(insights)
.where(eq(insights.userId, userId))
.orderBy(desc(insights.createdAt))
.limit(limit);
},
async findById(id: string): Promise<Insight | undefined> {
const rows = await db.select().from(insights).where(eq(insights.id, id));
return rows[0];
},
async create(data: NewInsight): Promise<Insight> {
const rows = await db.insert(insights).values(data).returning();
return rows[0]!;
},
async update(id: string, data: Partial<NewInsight>): Promise<Insight | undefined> {
const rows = await db
.update(insights)
.set({ ...data, updatedAt: new Date() })
.where(eq(insights.id, id))
.returning();
return rows[0];
},
async remove(id: string): Promise<boolean> {
const rows = await db.delete(insights).where(eq(insights.id, id)).returning();
return rows.length > 0;
},
/**
* Semantic search using pgvector cosine distance.
* Requires the vector extension and an embedding for the query.
*/
async searchByEmbedding(
userId: string,
queryEmbedding: number[],
limit = 10,
maxDistance = 0.8,
): Promise<SearchResult[]> {
const embeddingStr = `[${queryEmbedding.join(',')}]`;
const rows = await db.execute(sql`
SELECT *,
(embedding <=> ${embeddingStr}::vector) AS distance
FROM insights
WHERE user_id = ${userId}
AND embedding IS NOT NULL
AND (embedding <=> ${embeddingStr}::vector) < ${maxDistance}
ORDER BY distance ASC
LIMIT ${limit}
`);
return rows as unknown as SearchResult[];
},
/**
* Decay relevance scores for old insights that haven't been accessed recently.
*/
async decayOldInsights(olderThan: Date, decayFactor = 0.95): Promise<number> {
const result = await db
.update(insights)
.set({
relevanceScore: sql`${insights.relevanceScore} * ${decayFactor}`,
decayedAt: new Date(),
updatedAt: new Date(),
})
.where(and(lt(insights.updatedAt, olderThan), sql`${insights.relevanceScore} > 0.1`))
.returning();
return result.length;
},
};
}
export type InsightsRepo = ReturnType<typeof createInsightsRepo>;