From a7efd817ec22a97c935d89c811121327feb8080e Mon Sep 17 00:00:00 2001 From: Abhay Singh Date: Wed, 12 Aug 2026 19:35:45 +0530 Subject: [PATCH 1/7] fix(validation): reject non-positive page/limit in pagination query schemas (#1271) --- packages/validation/api.test.ts | 74 ++++++++++++++++++++++++++++++++- packages/validation/api.ts | 10 ++++- 2 files changed, 81 insertions(+), 3 deletions(-) diff --git a/packages/validation/api.test.ts b/packages/validation/api.test.ts index 05c18934c..e186af88f 100644 --- a/packages/validation/api.test.ts +++ b/packages/validation/api.test.ts @@ -1,6 +1,11 @@ import { describe, expect, it } from "bun:test" import { readFileSync } from "node:fs" -import { SearchRequestSchema, Searchv4RequestSchema } from "./api" +import { + DocumentsWithMemoriesQuerySchema, + ListMemoriesQuerySchema, + SearchRequestSchema, + Searchv4RequestSchema, +} from "./api" describe("search threshold schemas", () => { it("do not contain redundant number transforms or unreachable range guards", () => { @@ -80,3 +85,70 @@ describe("search threshold schemas", () => { ).toBe(false) }) }) + +describe("pagination query schemas", () => { + it("preserve page/limit defaults", () => { + const listed = ListMemoriesQuerySchema.parse({}) + expect(listed.page).toBe(1) + expect(listed.limit).toBe(10) + + const docs = DocumentsWithMemoriesQuerySchema.parse({}) + expect(docs.page).toBe(1) + expect(docs.limit).toBe(10) + }) + + it.each([ + 1, 50, 1100, + ])("ListMemoriesQuerySchema accepts numeric limit %p", (limit) => { + expect(ListMemoriesQuerySchema.parse({ limit }).limit).toBe(limit) + }) + + it("ListMemoriesQuerySchema accepts numeric string page/limit", () => { + const parsed = ListMemoriesQuerySchema.parse({ page: "3", limit: "25" }) + expect(parsed.page).toBe(3) + expect(parsed.limit).toBe(25) + }) + + it.each([ + 0, -5, 2.5, + ])("ListMemoriesQuerySchema rejects non-positive or fractional numeric limit %p", (limit) => { + expect(ListMemoriesQuerySchema.safeParse({ limit }).success).toBe(false) + }) + + it.each([ + 0, -1, 1.5, + ])("ListMemoriesQuerySchema rejects non-positive or fractional numeric page %p", (page) => { + expect(ListMemoriesQuerySchema.safeParse({ page }).success).toBe(false) + }) + + it("ListMemoriesQuerySchema still caps limit at 1100", () => { + expect(ListMemoriesQuerySchema.safeParse({ limit: 1101 }).success).toBe( + false, + ) + }) + + it.each([ + 0, -1, 2.5, + ])("DocumentsWithMemoriesQuerySchema rejects invalid page %p", (page) => { + expect(DocumentsWithMemoriesQuerySchema.safeParse({ page }).success).toBe( + false, + ) + }) + + it.each([ + 0, -10, 2.5, + ])("DocumentsWithMemoriesQuerySchema rejects invalid limit %p", (limit) => { + expect(DocumentsWithMemoriesQuerySchema.safeParse({ limit }).success).toBe( + false, + ) + }) + + it("DocumentsWithMemoriesQuerySchema accepts a normal request", () => { + const parsed = DocumentsWithMemoriesQuerySchema.parse({ + page: 2, + limit: 50, + }) + expect(parsed.page).toBe(2) + expect(parsed.limit).toBe(50) + }) +}) diff --git a/packages/validation/api.ts b/packages/validation/api.ts index f689dbf73..f066bfcd4 100644 --- a/packages/validation/api.ts +++ b/packages/validation/api.ts @@ -275,6 +275,9 @@ export const ListMemoriesQuerySchema = z .regex(/^\d+$/) .or(z.number()) .transform(Number) + .refine((value) => Number.isInteger(value) && value >= 1, { + message: "Limit must be a positive integer", + }) .refine((value) => value <= 1100, { message: "Limit cannot be greater than 1100", }) @@ -292,6 +295,9 @@ export const ListMemoriesQuerySchema = z .regex(/^\d+$/) .or(z.number()) .transform(Number) + .refine((value) => Number.isInteger(value) && value >= 1, { + message: "Page must be a positive integer", + }) .default("1") .openapi({ description: "Page number to fetch", example: "1" }), sort: z @@ -1092,11 +1098,11 @@ export const DocumentsWithMemoriesResponseSchema = z export const DocumentsWithMemoriesQuerySchema = z .object({ - page: z.number().default(1).openapi({ + page: z.number().int().min(1).default(1).openapi({ description: "Page number to fetch", example: 1, }), - limit: z.number().default(10).openapi({ + limit: z.number().int().min(1).default(10).openapi({ description: "Number of items per page", example: 10, }), From f163c932cfac28ab04103ccef8f3de73f220c99d Mon Sep 17 00:00:00 2001 From: Abhay Singh Date: Wed, 12 Aug 2026 20:55:01 +0530 Subject: [PATCH 2/7] fix(web): keep highlights card active index in range on refresh (#1334) Co-authored-by: Vedant Mahajan --- apps/web/components/highlights-card.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/components/highlights-card.tsx b/apps/web/components/highlights-card.tsx index 38257d2e2..6caaadf73 100644 --- a/apps/web/components/highlights-card.tsx +++ b/apps/web/components/highlights-card.tsx @@ -92,8 +92,8 @@ export function HighlightsCard({ if (isReplyOpen) replyInputRef.current?.focus() }, [isReplyOpen]) - // biome-ignore lint/correctness/useExhaustiveDependencies: intentionally re-run when items changes useEffect(() => { + setActiveIndex((i) => Math.min(i, Math.max(items.length - 1, 0))) setIsReplyOpen(false) setReplyText("") setIsExpanded(false) From 74b2201eebe9ff44f37bc985682888dceb509cd2 Mon Sep 17 00:00:00 2001 From: Abhay Singh Date: Wed, 12 Aug 2026 20:55:53 +0530 Subject: [PATCH 3/7] fix(memory-graph): stop painting expired memories as expiring (#1335) Co-authored-by: Vedant Mahajan --- .../memory-graph/src/__tests__/graph-data-utils.test.ts | 7 +++++++ packages/memory-graph/src/hooks/use-graph-data.ts | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/memory-graph/src/__tests__/graph-data-utils.test.ts b/packages/memory-graph/src/__tests__/graph-data-utils.test.ts index 4f728f97b..f98df074a 100644 --- a/packages/memory-graph/src/__tests__/graph-data-utils.test.ts +++ b/packages/memory-graph/src/__tests__/graph-data-utils.test.ts @@ -65,6 +65,13 @@ describe("getMemoryBorderColor", () => { expect(getMemoryBorderColor(mem, colors)).toBe(colors.memBorderExpiring) }) + it("does not treat an already-elapsed forgetAfter as expiring", () => { + const past = new Date(Date.now() - 60 * 1000).toISOString() + const old = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString() + const mem = makeMemory({ forgetAfter: past, createdAt: old }) + expect(getMemoryBorderColor(mem, colors)).toBe(colors.memStrokeDefault) + }) + it("returns recent color for memories created within 24 hours", () => { const recent = new Date(Date.now() - 1000).toISOString() const mem = makeMemory({ createdAt: recent }) diff --git a/packages/memory-graph/src/hooks/use-graph-data.ts b/packages/memory-graph/src/hooks/use-graph-data.ts index 00a05d10c..aed2d6f01 100644 --- a/packages/memory-graph/src/hooks/use-graph-data.ts +++ b/packages/memory-graph/src/hooks/use-graph-data.ts @@ -47,7 +47,7 @@ export function getMemoryBorderColor( if (mem.isForgotten) return colors.memBorderForgotten if (mem.forgetAfter) { const msLeft = new Date(mem.forgetAfter).getTime() - Date.now() - if (msLeft < SEVEN_DAYS_MS) return colors.memBorderExpiring + if (msLeft > 0 && msLeft < SEVEN_DAYS_MS) return colors.memBorderExpiring } const age = Date.now() - new Date(mem.createdAt).getTime() if (age < ONE_DAY_MS) return colors.memBorderRecent From 14bcc92c31b86dc9b8b74c1aadacd45fc6308d6b Mon Sep 17 00:00:00 2001 From: Abhay Singh Date: Wed, 12 Aug 2026 20:56:23 +0530 Subject: [PATCH 4/7] fix(memory-graph): center arrow-key navigation in the visible graph area (#1337) --- packages/memory-graph/src/components/memory-graph.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/memory-graph/src/components/memory-graph.tsx b/packages/memory-graph/src/components/memory-graph.tsx index 9d0c2ef65..d165ac718 100644 --- a/packages/memory-graph/src/components/memory-graph.tsx +++ b/packages/memory-graph/src/components/memory-graph.tsx @@ -467,10 +467,10 @@ export function MemoryGraph({ n.x, n.y, containerSize.width, - containerSize.height, + graphFitHeight, ) }, - [nodes, containerSize.width, containerSize.height], + [nodes, containerSize.width, graphFitHeight], ) const navigateUp = useCallback(() => { From 47152afc1d512d4247b31bdda05ea4efc19f6c96 Mon Sep 17 00:00:00 2001 From: pawan Date: Wed, 12 Aug 2026 20:56:54 +0530 Subject: [PATCH 5/7] fix(openai-sdk): cap supermemory to <3.5 so a fresh install imports (#1236) --- packages/openai-sdk-python/pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/openai-sdk-python/pyproject.toml b/packages/openai-sdk-python/pyproject.toml index 557e38b04..4fcdf9819 100644 --- a/packages/openai-sdk-python/pyproject.toml +++ b/packages/openai-sdk-python/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "supermemory-openai-sdk" -version = "1.0.4" +version = "1.0.5" description = "Memory tools for OpenAI function calling with supermemory" readme = "README.md" license = "MIT" @@ -26,7 +26,7 @@ classifiers = [ requires-python = ">=3.8.1" dependencies = [ "openai>=1.102.0", - "supermemory>=3.1.0", + "supermemory>=3.1.0,<3.5.0", "typing-extensions>=4.0.0", "requests>=2.25.0", ] From 00e57fb9c219f353654b65017288de78d783b9ec Mon Sep 17 00:00:00 2001 From: Abhay Singh Date: Wed, 12 Aug 2026 20:57:33 +0530 Subject: [PATCH 6/7] fix(web): stop formatUsageNumber rendering 1000.0K at unit boundaries (#1340) Co-authored-by: Vedant Mahajan --- apps/web/lib/billing-utils.ts | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/apps/web/lib/billing-utils.ts b/apps/web/lib/billing-utils.ts index 3360e9c41..c2f7cfaa8 100644 --- a/apps/web/lib/billing-utils.ts +++ b/apps/web/lib/billing-utils.ts @@ -185,18 +185,26 @@ export function getBrainTrialInfo( } /** - * Format a number with K/M suffix for display + * Format a number with K/M/B suffix for display * @example formatUsageNumber(1500000) => "1.5M" * @example formatUsageNumber(50000) => "50K" + * @example formatUsageNumber(999950) => "1.0M" */ export function formatUsageNumber(value: number): string { + const withSuffix = (n: number, suffix: string) => + n % 1 === 0 ? `${n}${suffix}` : `${n.toFixed(1)}${suffix}` + if (value >= 1_000_000) { const millions = value / 1_000_000 - return millions % 1 === 0 ? `${millions}M` : `${millions.toFixed(1)}M` + return millions >= 999.95 + ? withSuffix(value / 1_000_000_000, "B") + : withSuffix(millions, "M") } if (value >= 1_000) { const thousands = value / 1_000 - return thousands % 1 === 0 ? `${thousands}K` : `${thousands.toFixed(1)}K` + return thousands >= 999.95 + ? withSuffix(value / 1_000_000, "M") + : withSuffix(thousands, "K") } return value.toString() } From b7a6ea9a5f1f287a2e323b6125769b6aadf28959 Mon Sep 17 00:00:00 2001 From: Abhay Singh Date: Wed, 12 Aug 2026 20:58:06 +0530 Subject: [PATCH 7/7] fix(extension): stop fragmenting Included Memories that contain commas or newlines (#1339) Co-authored-by: Vedant Mahajan --- .../entrypoints/content/chatgpt.ts | 5 ++- .../entrypoints/content/claude.ts | 5 ++- .../entrypoints/content/gemini.ts | 5 ++- .../entrypoints/content/memory-suggestion.ts | 38 +++++++++++++++++-- .../entrypoints/content/t3.ts | 28 ++++++++------ 5 files changed, 63 insertions(+), 18 deletions(-) diff --git a/apps/browser-extension/entrypoints/content/chatgpt.ts b/apps/browser-extension/entrypoints/content/chatgpt.ts index 444e3ac87..cf7a30028 100644 --- a/apps/browser-extension/entrypoints/content/chatgpt.ts +++ b/apps/browser-extension/entrypoints/content/chatgpt.ts @@ -17,6 +17,7 @@ import { acceptMemorySuggestion, clearMemorySuggestion, hasAcceptedSupermemoryContext, + serializeMemoriesForDataset, setMemoryMarkerStatus, showLoadingSuggestion, showMarkerPopover, @@ -212,7 +213,9 @@ async function getRelatedMemoriesForChatGPT(actionSource: string) { memoryLength: memoryText.length, }) - iconElement.dataset.memoriesData = String(response.data) + iconElement.dataset.memoriesData = serializeMemoriesForDataset( + response.data, + ) if (isAutoSearch) { setMemoryMarkerStatus(iconElement, "found") diff --git a/apps/browser-extension/entrypoints/content/claude.ts b/apps/browser-extension/entrypoints/content/claude.ts index 7bff4dfc2..f31c2bb60 100644 --- a/apps/browser-extension/entrypoints/content/claude.ts +++ b/apps/browser-extension/entrypoints/content/claude.ts @@ -17,6 +17,7 @@ import { acceptMemorySuggestion, clearMemorySuggestion, hasAcceptedSupermemoryContext, + serializeMemoriesForDataset, setMemoryMarkerStatus, showLoadingSuggestion, showMarkerPopover, @@ -459,7 +460,9 @@ async function getRelatedMemoriesForClaude(actionSource: string) { memoryLength: memoryText.length, }) - iconElement.dataset.memoriesData = String(response.data) + iconElement.dataset.memoriesData = serializeMemoriesForDataset( + response.data, + ) if (isAutoSearch) { setMemoryMarkerStatus(iconElement, "found") diff --git a/apps/browser-extension/entrypoints/content/gemini.ts b/apps/browser-extension/entrypoints/content/gemini.ts index 6ece78dff..f819d3d6f 100644 --- a/apps/browser-extension/entrypoints/content/gemini.ts +++ b/apps/browser-extension/entrypoints/content/gemini.ts @@ -17,6 +17,7 @@ import { acceptMemorySuggestion, clearMemorySuggestion, hasAcceptedSupermemoryContext, + serializeMemoriesForDataset, setMemoryMarkerStatus, showLoadingSuggestion, showMarkerPopover, @@ -417,7 +418,9 @@ async function getRelatedMemoriesForGemini(actionSource: string) { if (response?.success && response?.data && input) { const memoryText = showMemorySuggestion("gemini", input, response.data) - iconElement.dataset.memoriesData = String(response.data) + iconElement.dataset.memoriesData = serializeMemoriesForDataset( + response.data, + ) iconElement.dataset.supermemories = memoryText if (isAutoSearch) { setMemoryMarkerStatus(iconElement, "found") diff --git a/apps/browser-extension/entrypoints/content/memory-suggestion.ts b/apps/browser-extension/entrypoints/content/memory-suggestion.ts index 1722e71ef..18861fe30 100644 --- a/apps/browser-extension/entrypoints/content/memory-suggestion.ts +++ b/apps/browser-extension/entrypoints/content/memory-suggestion.ts @@ -12,6 +12,39 @@ export function buildSupermemoryText(memories: unknown): string { return `\n\n${SUPERMEMORY_PREFIX} ${memoryText}` } +function normalizeMemoryList(memories: unknown): string[] { + const list = Array.isArray(memories) + ? memories + : memories == null + ? [] + : [memories] + return list + .map((memory) => (typeof memory === "string" ? memory : String(memory))) + .map((memory) => memory.trim()) + .filter((memory) => memory.length > 0) +} + +export function serializeMemoriesForDataset(memories: unknown): string { + const list = normalizeMemoryList(memories) + return list.length > 0 ? JSON.stringify(list) : "" +} + +export function parseMemoriesFromDataset( + raw: string | null | undefined, +): string[] { + if (!raw) return [] + try { + const parsed = JSON.parse(raw) + if (Array.isArray(parsed)) return normalizeMemoryList(parsed) + } catch { + // Not JSON — fall through to the legacy delimiter split. + } + return raw + .split(/[,\n]/) + .map((memory) => memory.trim()) + .filter((memory) => memory.length > 0 && memory !== ",") +} + export function showMemorySuggestion( platform: string, input: SuggestionInput, @@ -305,10 +338,7 @@ export function showMarkerPopover( color: rgba(255, 255, 255, 0.76); ` - memories - .split(/[,\n]/) - .map((memory) => memory.trim()) - .filter((memory) => memory.length > 0 && memory !== ",") + parseMemoriesFromDataset(memories) .slice(0, 5) .forEach((memory) => { const item = document.createElement("div") diff --git a/apps/browser-extension/entrypoints/content/t3.ts b/apps/browser-extension/entrypoints/content/t3.ts index 66a11235c..39a05f03e 100644 --- a/apps/browser-extension/entrypoints/content/t3.ts +++ b/apps/browser-extension/entrypoints/content/t3.ts @@ -10,6 +10,10 @@ import { autoCapturePromptsEnabled, } from "../../utils/storage" import { createT3InputBarElement, DOMUtils } from "../../utils/ui-components" +import { + parseMemoriesFromDataset, + serializeMemoriesForDataset, +} from "./memory-suggestion" let t3DebounceTimeout: NodeJS.Timeout | null = null let t3RouteObserver: MutationObserver | null = null @@ -233,7 +237,9 @@ async function getRelatedMemoriesForT3(actionSource: string) { if (textareaElement) { textareaElement.dataset.supermemories = `\n\nSupermemories of user (only for the reference): ${response.data}` - iconElement.dataset.memoriesData = response.data + iconElement.dataset.memoriesData = serializeMemoriesForDataset( + response.data, + ) updateT3IconFeedback("Included Memories", iconElement) } else { @@ -329,11 +335,9 @@ function updateT3IconFeedback( overflow-y: auto; ` - const memoriesText = iconElement.dataset.memoriesData || "" - const individualMemories = memoriesText - .split(/[,\n]/) - .map((memory) => memory.trim()) - .filter((memory) => memory.length > 0 && memory !== ",") + const individualMemories = parseMemoriesFromDataset( + iconElement.dataset.memoriesData, + ) individualMemories.forEach((memory, index) => { const memoryItem = document.createElement("div") @@ -421,15 +425,17 @@ function updateT3IconFeedback( content.removeChild(memoryItem) } - const currentMemories = (iconElement.dataset.memoriesData || "") - .split(/[,\n]/) - .map((memory) => memory.trim()) - .filter((memory) => memory.length > 0 && memory !== ",") + const currentMemories = parseMemoriesFromDataset( + iconElement.dataset.memoriesData, + ) currentMemories.splice(index, 1) + // Injected prompt keeps its existing joined-text form; the popup's + // own data is stored as JSON so comma-bearing memories stay intact. const updatedMemories = currentMemories.join(" ,") - iconElement.dataset.memoriesData = updatedMemories + iconElement.dataset.memoriesData = + serializeMemoriesForDataset(currentMemories) const textareaElement = (document.querySelector("textarea") as HTMLTextAreaElement) ||