Skip to content

Commit abc6c54

Browse files
committed
add version history API: GET /memories/:id/versions and /agent/memories/:id/versions
Exposes the existing memory_versions snapshots through new read endpoints. Dashboard and agent routes both supported. OpenAPI spec updated (14 ops). Made-with: Cursor
1 parent 857090c commit abc6c54

3 files changed

Lines changed: 143 additions & 0 deletions

File tree

openapi-agent.yaml

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ servers:
2020
- url: https://api.reflectmemory.com
2121

2222
components:
23+
schemas: {}
2324
securitySchemes:
2425
oauth:
2526
type: oauth2
@@ -278,6 +279,48 @@ paths:
278279
description: Cannot delete a memory you did not create
279280
"404":
280281
description: Memory not found or already deleted
282+
/agent/memories/{id}/versions:
283+
get:
284+
operationId: getVersionHistory
285+
x-openai-isConsequential: false
286+
summary: Get edit history for a memory
287+
description: >
288+
Returns all previous versions of a memory in reverse chronological order.
289+
Each version is a full snapshot captured before an edit.
290+
Use to review changes, audit edits, or compare past states.
291+
parameters:
292+
- name: id
293+
in: path
294+
required: true
295+
schema:
296+
type: string
297+
description: The memory UUID
298+
responses:
299+
"200":
300+
description: Version history
301+
content:
302+
application/json:
303+
schema:
304+
type: object
305+
properties:
306+
versions:
307+
type: array
308+
items:
309+
type: object
310+
properties:
311+
id: { type: string }
312+
memory_id: { type: string }
313+
title: { type: string }
314+
content: { type: string }
315+
tags: { type: array, items: { type: string } }
316+
origin: { type: string }
317+
version_number: { type: integer }
318+
created_at: { type: string }
319+
current_version:
320+
type: integer
321+
description: The current version number (versions + 1)
322+
"404":
323+
description: Memory not found or not accessible
281324
/agent/memories/search:
282325
post:
283326
operationId: searchMemories

src/memory-service.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -801,3 +801,49 @@ export function countTeamMemories(
801801
.get(teamId) as { cnt: number };
802802
return row.cnt;
803803
}
804+
805+
export interface MemoryVersion {
806+
id: string;
807+
memory_id: string;
808+
title: string;
809+
content: string;
810+
tags: string[];
811+
origin: string;
812+
allowed_vendors: string[];
813+
memory_type: MemoryType;
814+
version_number: number;
815+
created_at: string;
816+
}
817+
818+
export function getVersionHistory(
819+
db: Database.Database,
820+
userId: string,
821+
memoryId: string,
822+
): MemoryVersion[] {
823+
const owner = db
824+
.prepare(`SELECT id FROM memories WHERE id = ? AND user_id = ?`)
825+
.get(memoryId, userId);
826+
if (!owner) return [];
827+
828+
const rows = db
829+
.prepare(
830+
`SELECT id, memory_id, title, content, tags, memory_type, origin, allowed_vendors, version_number, created_at
831+
FROM memory_versions
832+
WHERE memory_id = ?
833+
ORDER BY version_number DESC`,
834+
)
835+
.all(memoryId) as Array<Record<string, string | number>>;
836+
837+
return rows.map((row) => ({
838+
id: row.id as string,
839+
memory_id: row.memory_id as string,
840+
title: row.title as string,
841+
content: row.content as string,
842+
tags: safeJsonArray(row.tags as string),
843+
origin: row.origin as string,
844+
allowed_vendors: safeJsonArray(row.allowed_vendors as string),
845+
memory_type: (row.memory_type as MemoryType) || "semantic",
846+
version_number: row.version_number as number,
847+
created_at: row.created_at as string,
848+
}));
849+
}

src/server.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ import {
6969
unshareMemory,
7070
listTeamMemories,
7171
countTeamMemories,
72+
getVersionHistory,
7273
} from "./memory-service.js";
7374
import { buildPrompt, type PromptResult } from "./context-builder.js";
7475
import {
@@ -1609,6 +1610,35 @@ export async function createServer(config: ServerConfig): Promise<FastifyInstanc
16091610
},
16101611
);
16111612

1613+
// ===========================================================================
1614+
// GET /agent/memories/:id/versions -- Version history for agents
1615+
// ===========================================================================
1616+
1617+
server.get(
1618+
"/agent/memories/:id/versions",
1619+
{
1620+
schema: { params: memoryIdParamSchema },
1621+
config: { rateLimit: { max: 30, timeWindow: "1 minute" } },
1622+
},
1623+
async (request, reply) => {
1624+
const { id } = request.params as { id: string };
1625+
const memory = readMemoryById(db, request.userId, id);
1626+
if (!memory || memory.deleted_at) {
1627+
reply.code(404);
1628+
return { error: "Memory not found" };
1629+
}
1630+
if (request.vendor) {
1631+
const allowed = memory.allowed_vendors.includes("*") || memory.allowed_vendors.includes(request.vendor);
1632+
if (!allowed) {
1633+
reply.code(404);
1634+
return { error: "Memory not found" };
1635+
}
1636+
}
1637+
const versions = getVersionHistory(db, request.userId, id);
1638+
return { versions, current_version: versions.length + 1 };
1639+
},
1640+
);
1641+
16121642
// ===========================================================================
16131643
// PUT /agent/memories/:id -- Update a memory (agent path)
16141644
// ===========================================================================
@@ -1973,6 +2003,30 @@ export async function createServer(config: ServerConfig): Promise<FastifyInstanc
19732003
},
19742004
);
19752005

2006+
// ===========================================================================
2007+
// GET /memories/:id/versions -- Version history for a memory (dashboard)
2008+
// ===========================================================================
2009+
2010+
server.get(
2011+
"/memories/:id/versions",
2012+
{
2013+
schema: { params: memoryIdParamSchema },
2014+
config: { rateLimit: { max: 30, timeWindow: "1 minute" } },
2015+
},
2016+
async (request, reply) => {
2017+
const { id } = request.params as { id: string };
2018+
const versions = getVersionHistory(db, request.userId, id);
2019+
if (versions.length === 0) {
2020+
const memory = readMemoryById(db, request.userId, id);
2021+
if (!memory) {
2022+
reply.code(404);
2023+
return { error: "Memory not found" };
2024+
}
2025+
}
2026+
return { versions };
2027+
},
2028+
);
2029+
19762030
// ===========================================================================
19772031
// POST /memories/list -- List memories with an explicit filter
19782032
// ===========================================================================

0 commit comments

Comments
 (0)