TypeScript Conversation Forking
This guide adds conversation forking metadata to the chat path.
New to forking concepts? Read Forking first.
Prerequisites
Starting checkpoint: typescript/examples/vecelai/doc-checkpoints/03-with-history
Add Fork Parameters to Chat
Checkpoint 04 adds fork query parsing:
const forkedAtConversationId =
(req.query.forkedAtConversationId as string | undefined) ?? null;
const forkedAtEntryId =
(req.query.forkedAtEntryId as string | undefined) ?? null; What changed: The endpoint accepts optional forkedAtConversationId and forkedAtEntryId query parameters.
Why needed: These values identify the source conversation and exact branch point for creating a new forked branch.
Pass Fork Metadata to the USER History Append
const result = await withMemoryService(
{
...memoryServiceConfig,
conversationId,
authorization,
memoryContentType: "vercelai",
userText: userMessage,
forkedAtConversationId,
forkedAtEntryId,
},
async (contextMemory) => { What changed: The chat handler now passes fork metadata (forkedAtConversationId, forkedAtEntryId) into withMemoryService(...), so only the initial USER history write carries the fork link.
Why needed: Memory Service creates the fork relationship from that first USER history append in the new conversation; later writes should not repeat fork metadata.
Proxy the Fork Listing Endpoint
memoryServiceConfigFromEnv,
withMemoryService,
withProxy,
} from "@chirino/memory-service-vercelai";
const app = express();
app.use(express.text({ type: "*/*" })); What changed: Adds memoryServiceConfigFromEnv(...), withProxy, and a shared memoryServiceConfig.
Why needed: Fork-listing routes proxy Memory Service APIs through the app layer, and the proxy now receives explicit SDK config instead of reading env vars implicitly.
function asNumber(value: unknown): number | null {
if (typeof value !== "string" || value === "") {
return null;
}
const parsed = Number(value);
return Number.isFinite(parsed) ? parsed : null;
} What changed: Adds a small query coercion helper for paging params.
Why needed: Query values arrive as strings; the proxy API expects numeric limit or null.
app.get("/v1/conversations/:conversationId/forks", async (req, res) => {
await withProxy(req, res, memoryServiceConfig, (proxy) =>
proxy.listConversationForks(req.params.conversationId, {
afterCursor: (req.query.afterCursor as string | undefined) ?? null,
limit: asNumber(req.query.limit),
}),
);
}); What changed: Exposes GET /v1/conversations/:conversationId/forks and forwards pagination args.
Why needed: Clients can discover and render branch trees without calling Memory Service directly.
Try It With Curl
Define a helper to get a bearer token for bob:
function get-token() {
curl -sSfX POST http://localhost:8081/realms/memory-service/protocol/openid-connect/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "client_id=memory-service-client" \
-d "client_secret=change-me" \
-d "grant_type=password" \
-d "username=bob" \
-d "password=bob" \
| jq -r '.access_token'
}Create the source conversation turn:
curl -NsSfX POST http://localhost:9090/chat/c4d8a5f1-2f7b-4a6b-9e03-3a2f66e3b111 \
-H "Content-Type: text/plain" \
-H "Authorization: Bearer $(get-token)" \
-d "Hello from the root conversation." Example output:
Sure, I can help with that. Read the branch point entry id from Memory Service:
curl -sSfX GET http://localhost:8082/v1/conversations/c4d8a5f1-2f7b-4a6b-9e03-3a2f66e3b111/entries?channel=history \
-H "Authorization: Bearer $(get-token)" \
-H "X-API-Key: agent-api-key-1" | jq Example output:
{
"data": [
{
"id": "ff1c0c7b-f76f-495d-b73d-b88bd83de88f"
}
],
"afterCursor": null
} Create the forked conversation by calling chat with fork metadata:
curl -NsSfX POST "http://localhost:9090/chat/e5b9c1d2-8a4e-4c1a-b8d1-1d7f8e2a9222?forkedAtConversationId=c4d8a5f1-2f7b-4a6b-9e03-3a2f66e3b111&forkedAtEntryId=${FORK_ENTRY_ID}" \
-H "Content-Type: text/plain" \
-H "Authorization: Bearer $(get-token)" \
-d "Continue from this fork." Example output:
{
"afterCursor": null,
"data": [
{
"id": "ff1c0c7b-f76f-495d-b73d-b88bd83de88f",
"conversationId": "c4d8a5f1-2f7b-4a6b-9e03-3a2f66e3b111",
"userId": "bob",
"clientId": "checkpoint-agent",
"channel": "history",
"contentType": "history",
"createdAt": "2026-03-06T14:59:40.352461Z",
"content": [
{
"role": "USER",
"text": "Hello from the root conversation."
}
]
},
{
"id": "a370e1b2-953a-4767-aa38-69638621ff00",
"conversationId": "c4d8a5f1-2f7b-4a6b-9e03-3a2f66e3b111",
"userId": "bob",
"clientId": "checkpoint-agent",
"channel": "history",
"contentType": "history",
"createdAt": "2026-03-06T14:59:40.382506Z",
"content": [
{
"role": "AI",
"text": "I am a TypeScript memory-service demo agent."
}
]
}
]
} List forks through the proxied endpoint:
curl -sSfX GET http://localhost:9090/v1/conversations/c4d8a5f1-2f7b-4a6b-9e03-3a2f66e3b111/forks \
-H "Authorization: Bearer $(get-token)" | jq Example output:
{
"data": [
{
"conversationId": "c4d8a5f1-2f7b-4a6b-9e03-3a2f66e3b111"
},
{
"conversationId": "e5b9c1d2-8a4e-4c1a-b8d1-1d7f8e2a9222"
}
],
"afterCursor": null
} Next Steps
- Response Recording and Resumption - Streaming with resume and cancel endpoints
- Sharing - Membership and ownership transfer APIs