Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -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: {
Expand Down
3 changes: 2 additions & 1 deletion src/services/api-handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<ShardInfo[]> {
Expand Down Expand Up @@ -915,7 +916,7 @@ export async function handleGetUserProfile(userId?: string): Promise<ApiResponse
message: "No profile found. Keep chatting to build your profile.",
},
};
const profileData = JSON.parse(profile.profileData);
const profileData = stripProfileEmbeddings(JSON.parse(profile.profileData));
profileData.preferences = sortProfileItems(profileData.preferences as any[], "confidence");
profileData.patterns = sortProfileItems(profileData.patterns as any[], "frequency");
profileData.workflows = sortProfileItems(profileData.workflows as any[], "frequency");
Expand Down
25 changes: 25 additions & 0 deletions src/services/user-profile/profile-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,31 @@ export const safeArray = <T>(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 = <T>(data: T): T => {
if (!data || typeof data !== "object") return data;
const container = data as Record<string, unknown>;
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<string, unknown>).centroid;
delete (item as Record<string, unknown>).anchor;
}
}
}
return data;
};

export const safeObject = <T extends object>(obj: any, fallback: T): T => {
if (!obj) return fallback;
let result = obj;
Expand Down
35 changes: 35 additions & 0 deletions tests/profile-utils.test.ts
Original file line number Diff line number Diff line change
@@ -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();
});
});