From 05a805eecae596272ce64148090e2e9634e2564d Mon Sep 17 00:00:00 2001 From: Jason Woltje Date: Sat, 21 Mar 2026 20:34:42 +0000 Subject: [PATCH] =?UTF-8?q?fix(memory):=20scope=20InsightsRepo=20operation?= =?UTF-8?q?s=20to=20userId=20=E2=80=94=20M2-001/002=20(#290)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Jason Woltje Co-committed-by: Jason Woltje --- apps/gateway/src/log/summarization.service.ts | 2 +- apps/gateway/src/memory/memory.controller.ts | 8 +-- packages/memory/src/insights.ts | 49 ++++++++++++++++--- 3 files changed, 47 insertions(+), 12 deletions(-) diff --git a/apps/gateway/src/log/summarization.service.ts b/apps/gateway/src/log/summarization.service.ts index 4cfa02b..d53fb7d 100644 --- a/apps/gateway/src/log/summarization.service.ts +++ b/apps/gateway/src/log/summarization.service.ts @@ -137,7 +137,7 @@ export class SummarizationService { const promoted = await this.logService.logs.promoteToCold(warmCutoff); const purged = await this.logService.logs.purge(coldCutoff); - const decayed = await this.memory.insights.decayOldInsights(decayCutoff); + const decayed = await this.memory.insights.decayAllInsights(decayCutoff); this.logger.log( `Tier management: ${promoted} logs→cold, ${purged} purged, ${decayed} insights decayed`, diff --git a/apps/gateway/src/memory/memory.controller.ts b/apps/gateway/src/memory/memory.controller.ts index 6aa1324..b2179ed 100644 --- a/apps/gateway/src/memory/memory.controller.ts +++ b/apps/gateway/src/memory/memory.controller.ts @@ -73,8 +73,8 @@ export class MemoryController { } @Get('insights/:id') - async getInsight(@Param('id') id: string) { - const insight = await this.memory.insights.findById(id); + async getInsight(@CurrentUser() user: { id: string }, @Param('id') id: string) { + const insight = await this.memory.insights.findById(id, user.id); if (!insight) throw new NotFoundException('Insight not found'); return insight; } @@ -97,8 +97,8 @@ export class MemoryController { @Delete('insights/:id') @HttpCode(HttpStatus.NO_CONTENT) - async removeInsight(@Param('id') id: string) { - const deleted = await this.memory.insights.remove(id); + async removeInsight(@CurrentUser() user: { id: string }, @Param('id') id: string) { + const deleted = await this.memory.insights.remove(id, user.id); if (!deleted) throw new NotFoundException('Insight not found'); } diff --git a/packages/memory/src/insights.ts b/packages/memory/src/insights.ts index d844caa..dd32f79 100644 --- a/packages/memory/src/insights.ts +++ b/packages/memory/src/insights.ts @@ -19,8 +19,11 @@ export function createInsightsRepo(db: Db) { .limit(limit); }, - async findById(id: string): Promise { - const rows = await db.select().from(insights).where(eq(insights.id, id)); + async findById(id: string, userId: string): Promise { + const rows = await db + .select() + .from(insights) + .where(and(eq(insights.id, id), eq(insights.userId, userId))); return rows[0]; }, @@ -29,17 +32,24 @@ export function createInsightsRepo(db: Db) { return rows[0]!; }, - async update(id: string, data: Partial): Promise { + async update( + id: string, + userId: string, + data: Partial, + ): Promise { const rows = await db .update(insights) .set({ ...data, updatedAt: new Date() }) - .where(eq(insights.id, id)) + .where(and(eq(insights.id, id), eq(insights.userId, userId))) .returning(); return rows[0]; }, - async remove(id: string): Promise { - const rows = await db.delete(insights).where(eq(insights.id, id)).returning(); + async remove(id: string, userId: string): Promise { + const rows = await db + .delete(insights) + .where(and(eq(insights.id, id), eq(insights.userId, userId))) + .returning(); return rows.length > 0; }, @@ -70,8 +80,33 @@ export function createInsightsRepo(db: Db) { /** * Decay relevance scores for old insights that haven't been accessed recently. + * Scoped to a specific user to prevent cross-user data mutation. */ - async decayOldInsights(olderThan: Date, decayFactor = 0.95): Promise { + async decayOldInsights(userId: string, olderThan: Date, decayFactor = 0.95): Promise { + const result = await db + .update(insights) + .set({ + relevanceScore: sql`${insights.relevanceScore} * ${decayFactor}`, + decayedAt: new Date(), + updatedAt: new Date(), + }) + .where( + and( + eq(insights.userId, userId), + lt(insights.updatedAt, olderThan), + sql`${insights.relevanceScore} > 0.1`, + ), + ) + .returning(); + return result.length; + }, + + /** + * Decay relevance scores for all users' old insights. + * This is a system-level maintenance operation intended for scheduled cron jobs only. + * Do NOT expose this through user-facing API endpoints. + */ + async decayAllInsights(olderThan: Date, decayFactor = 0.95): Promise { const result = await db .update(insights) .set({