diff --git a/src/index.ts b/src/index.ts index 326a69d3..bccbf0cc 100644 --- a/src/index.ts +++ b/src/index.ts @@ -757,6 +757,8 @@ export const OpenCodeMemPlugin: Plugin = async (ctx: PluginInput) => { const { userProfileManager } = await import("./services/user-profile/user-profile-manager.js"); + const { stripProfileEmbeddings } = + await import("./services/user-profile/profile-utils.js"); const userId = tags.user.userEmail || "unknown"; @@ -833,7 +835,7 @@ export const OpenCodeMemPlugin: Plugin = async (ctx: PluginInput) => { // --- READ: no content provided --- const profile = await userProfileManager.getActiveProfile(userId); if (!profile) return JSON.stringify({ success: true, profile: null }); - const pData = JSON.parse(profile.profileData); + const pData = stripProfileEmbeddings(JSON.parse(profile.profileData)); return JSON.stringify({ success: true, profile: { diff --git a/src/services/api-handlers.ts b/src/services/api-handlers.ts index 478e00e4..ca4b3375 100644 --- a/src/services/api-handlers.ts +++ b/src/services/api-handlers.ts @@ -11,6 +11,7 @@ import type { MemoryType } from "../types/index.js"; import { userPromptManager } from "./user-prompt/user-prompt-manager.js"; import type { UserProfileData } from "./user-profile/types.js"; import { sortProfileItems } from "../utils/profile.js"; +import { stripProfileEmbeddings } from "./user-profile/profile-utils.js"; import type { ShardInfo } from "./turso/types.js"; async function getAllMemoryShards(): Promise { @@ -915,7 +916,7 @@ export async function handleGetUserProfile(userId?: string): Promise(arr: any): T[] => { return flattened; }; +/** + * Remove per-item embedding vectors (`centroid`/`anchor`) from profile data. + * + * Those 768-dim vectors are only used internally for similarity, dedup and + * drift detection. Returning them to the model or the read-only API inflates + * the payload by hundreds of KB, so callers that serialize profile data for + * display must strip them first. Callers pass freshly parsed profile JSON, so + * the fields are deleted in place and the same object is returned. + */ +export const stripProfileEmbeddings = (data: T): T => { + if (!data || typeof data !== "object") return data; + const container = data as Record; + for (const key of ["preferences", "patterns", "workflows"]) { + const items = container[key]; + if (!Array.isArray(items)) continue; + for (const item of items) { + if (item && typeof item === "object") { + delete (item as Record).centroid; + delete (item as Record).anchor; + } + } + } + return data; +}; + export const safeObject = (obj: any, fallback: T): T => { if (!obj) return fallback; let result = obj; diff --git a/tests/profile-utils.test.ts b/tests/profile-utils.test.ts new file mode 100644 index 00000000..a8630d44 --- /dev/null +++ b/tests/profile-utils.test.ts @@ -0,0 +1,35 @@ +import { describe, expect, it } from "bun:test"; +import { stripProfileEmbeddings } from "../src/services/user-profile/profile-utils.js"; + +describe("stripProfileEmbeddings", () => { + it("removes centroid and anchor from every item type", () => { + const data = { + preferences: [{ description: "a", centroid: [1, 2], anchor: [3, 4], confidence: 0.5 }], + patterns: [{ description: "b", centroid: [1], anchor: [2], frequency: 3 }], + workflows: [{ description: "c", centroid: [1], anchor: [2], steps: ["x"] }], + }; + + const result = stripProfileEmbeddings(data); + + expect(result).toBe(data); + for (const key of ["preferences", "patterns", "workflows"] as const) { + for (const item of result[key]) { + expect(item.centroid).toBeUndefined(); + expect(item.anchor).toBeUndefined(); + } + } + expect(result.preferences[0].confidence).toBe(0.5); + expect(result.patterns[0].frequency).toBe(3); + expect(result.workflows[0].steps).toEqual(["x"]); + }); + + it("tolerates missing, malformed or non-object sections", () => { + expect(stripProfileEmbeddings(undefined as any)).toBeUndefined(); + expect(stripProfileEmbeddings(null as any)).toBeNull(); + expect(stripProfileEmbeddings({} as any)).toEqual({}); + expect(stripProfileEmbeddings({ preferences: "not-an-array" } as any)).toEqual({ + preferences: "not-an-array", + }); + expect(() => stripProfileEmbeddings({ patterns: [null, 1, "x"] } as any)).not.toThrow(); + }); +});