fix(memory): scope InsightsRepo operations to userId — M2-001/002 (#290)
Some checks failed
ci/woodpecker/push/ci Pipeline failed
Some checks failed
ci/woodpecker/push/ci Pipeline failed
Co-authored-by: Jason Woltje <jason@diversecanvas.com> Co-committed-by: Jason Woltje <jason@diversecanvas.com>
This commit was merged in pull request #290.
This commit is contained in:
@@ -137,7 +137,7 @@ export class SummarizationService {
|
|||||||
|
|
||||||
const promoted = await this.logService.logs.promoteToCold(warmCutoff);
|
const promoted = await this.logService.logs.promoteToCold(warmCutoff);
|
||||||
const purged = await this.logService.logs.purge(coldCutoff);
|
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(
|
this.logger.log(
|
||||||
`Tier management: ${promoted} logs→cold, ${purged} purged, ${decayed} insights decayed`,
|
`Tier management: ${promoted} logs→cold, ${purged} purged, ${decayed} insights decayed`,
|
||||||
|
|||||||
@@ -73,8 +73,8 @@ export class MemoryController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Get('insights/:id')
|
@Get('insights/:id')
|
||||||
async getInsight(@Param('id') id: string) {
|
async getInsight(@CurrentUser() user: { id: string }, @Param('id') id: string) {
|
||||||
const insight = await this.memory.insights.findById(id);
|
const insight = await this.memory.insights.findById(id, user.id);
|
||||||
if (!insight) throw new NotFoundException('Insight not found');
|
if (!insight) throw new NotFoundException('Insight not found');
|
||||||
return insight;
|
return insight;
|
||||||
}
|
}
|
||||||
@@ -97,8 +97,8 @@ export class MemoryController {
|
|||||||
|
|
||||||
@Delete('insights/:id')
|
@Delete('insights/:id')
|
||||||
@HttpCode(HttpStatus.NO_CONTENT)
|
@HttpCode(HttpStatus.NO_CONTENT)
|
||||||
async removeInsight(@Param('id') id: string) {
|
async removeInsight(@CurrentUser() user: { id: string }, @Param('id') id: string) {
|
||||||
const deleted = await this.memory.insights.remove(id);
|
const deleted = await this.memory.insights.remove(id, user.id);
|
||||||
if (!deleted) throw new NotFoundException('Insight not found');
|
if (!deleted) throw new NotFoundException('Insight not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,8 +19,11 @@ export function createInsightsRepo(db: Db) {
|
|||||||
.limit(limit);
|
.limit(limit);
|
||||||
},
|
},
|
||||||
|
|
||||||
async findById(id: string): Promise<Insight | undefined> {
|
async findById(id: string, userId: string): Promise<Insight | undefined> {
|
||||||
const rows = await db.select().from(insights).where(eq(insights.id, id));
|
const rows = await db
|
||||||
|
.select()
|
||||||
|
.from(insights)
|
||||||
|
.where(and(eq(insights.id, id), eq(insights.userId, userId)));
|
||||||
return rows[0];
|
return rows[0];
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -29,17 +32,24 @@ export function createInsightsRepo(db: Db) {
|
|||||||
return rows[0]!;
|
return rows[0]!;
|
||||||
},
|
},
|
||||||
|
|
||||||
async update(id: string, data: Partial<NewInsight>): Promise<Insight | undefined> {
|
async update(
|
||||||
|
id: string,
|
||||||
|
userId: string,
|
||||||
|
data: Partial<NewInsight>,
|
||||||
|
): Promise<Insight | undefined> {
|
||||||
const rows = await db
|
const rows = await db
|
||||||
.update(insights)
|
.update(insights)
|
||||||
.set({ ...data, updatedAt: new Date() })
|
.set({ ...data, updatedAt: new Date() })
|
||||||
.where(eq(insights.id, id))
|
.where(and(eq(insights.id, id), eq(insights.userId, userId)))
|
||||||
.returning();
|
.returning();
|
||||||
return rows[0];
|
return rows[0];
|
||||||
},
|
},
|
||||||
|
|
||||||
async remove(id: string): Promise<boolean> {
|
async remove(id: string, userId: string): Promise<boolean> {
|
||||||
const rows = await db.delete(insights).where(eq(insights.id, id)).returning();
|
const rows = await db
|
||||||
|
.delete(insights)
|
||||||
|
.where(and(eq(insights.id, id), eq(insights.userId, userId)))
|
||||||
|
.returning();
|
||||||
return rows.length > 0;
|
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.
|
* 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<number> {
|
async decayOldInsights(userId: string, 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(
|
||||||
|
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<number> {
|
||||||
const result = await db
|
const result = await db
|
||||||
.update(insights)
|
.update(insights)
|
||||||
.set({
|
.set({
|
||||||
|
|||||||
Reference in New Issue
Block a user