In the previous File Asset Manager chapter, we learned how to reliably move files between the user's computer and the cloud.
Now we have a Client that talks to Claude, and a File Manager that moves data. But here is a question: Does Claude remember what we said 5 minutes ago? By default, Large Language Models are "stateless"βthey forget everything immediately after answering.
Imagine playing a long video game. You play for 4 hours, defeat a boss, and collect a rare sword. Then, your power goes out. When you restart the game, you are back at the start menu. You lost everything.
Without Session State Sync, our CLI tool is like that video game without a memory card.
The Session State Sync module (located in sessionIngress.ts) is our "Auto-Save" and "Cloud Sync" engine.
It has two main jobs:
You are using the CLI to debug a script. You ask Claude to "Analyze this error."
As a developer using this API, you primarily interact with two functions: appendSessionLog (to save) and getSessionLogs (to load).
When a message is sent, we "append" it to the log.
import { appendSessionLog } from './sessionIngress.js';
const messageEntry = {
uuid: 'msg_123...', // Unique ID for this line
role: 'user',
content: 'Hello Claude!'
};
// Send to server
await appendSessionLog(sessionId, messageEntry, uploadUrl);
What happens here? The system queues this message and ensures it is written to the server in the correct order.
When the app starts, we need to hydrate the state.
import { getTeleportEvents } from './sessionIngress.js';
// Fetch the entire conversation history
const history = await getTeleportEvents(
sessionId,
token,
orgId
);
// Replay history to the user
history.forEach(entry => renderMessage(entry));
The hardest part of syncing isn't sending dataβit's sending data in the right order without corrupting the file, especially when the internet is bad.
Imagine a deli counter. You have ticket #45. The baker will only serve you after #44 is done.
The Session State Sync uses a similar concept called a Last-Uuid.
Let's look at sessionIngress.ts to see how this reliability is built.
We cannot send 5 messages in parallel. If "Step 2" arrives before "Step 1", the conversation makes no sense. We use a sequential wrapper to force a single-file line.
// inside sessionIngress.ts
function getOrCreateSequentialAppend(sessionId: string) {
// Check if we already have a queue for this session
let sequentialAppend = sequentialAppendBySession.get(sessionId);
if (!sequentialAppend) {
// Create a new queue that runs one item at a time
sequentialAppend = sequential(async (entry, url, headers) => {
return await appendSessionLogImpl(sessionId, entry, url, headers);
});
}
return sequentialAppend;
}
Just like the Resilient Request Executor, we wrap the save operation in a loop. If the server is busy (5xx) or the network drops, we try again.
// inside appendSessionLogImpl...
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
try {
// Attempt to save to the server
const response = await axios.put(url, entry, { headers });
if (response.status === 200) {
// Success! Update our local "Last-UUID" tracker
lastUuidMap.set(sessionId, entry.uuid);
return true;
}
// ... error handling below ...
} catch (error) {
// Wait a bit, then loop again
await sleep(calculateDelay(attempt));
}
}
This is the most critical part. A 409 Conflict means our local state is out of sync with the server.
if (response.status === 409) {
const serverLastUuid = response.headers['x-last-uuid'];
// Did we actually save it already? (Network glitch on response)
if (serverLastUuid === entry.uuid) {
return true; // It's actually fine!
}
// Someone else wrote to the log. Adopt their UUID and try again.
if (serverLastUuid) {
lastUuidMap.set(sessionId, serverLastUuid);
continue; // Retry the loop immediately with new info
}
}
When we fetch logs, we might get thousands of events. We need to handle "Pagination" (reading page by page).
export async function getTeleportEvents(sessionId, accessToken) {
const allEvents = [];
let cursor = undefined;
// Loop until the server says "No more pages"
while (true) {
const response = await axios.get(url, { params: { cursor } });
allEvents.push(...response.data.events);
// Check if there is a next page
if (!response.data.next_cursor) break;
cursor = response.data.next_cursor;
}
return allEvents;
}
In this chapter, we explored Session State Sync.
Now that we have a memory of what we sent, we need to manage the cost and size of that memory. Large conversations use many "tokens," which can get expensive and slow.
Next Chapter: Prompt Cache Monitor
Generated by Code IQ