Session Management & State Persistence
The Session Lifecycle
Every agent session follows a predictable lifecycle:
Startup Phase
The startup phase is critical for context recovery:
interface SessionStartup {
// Required checks
verifyEnvironment(): Promise<EnvironmentStatus>;
loadPreviousState(): Promise<SessionState>;
identifyNextTask(): Promise<Task>;
// Optional initialization
startDevServer?(): Promise<void>;
runHealthChecks?(): Promise<HealthCheckResult>;
}
async function sessionStartup(projectDir: string): Promise<SessionContext> {
console.log('š Starting new session...');
// 1. Verify we're in the right place
const cwd = await bash('pwd');
if (!cwd.includes(projectDir)) {
throw new Error(`Wrong directory: ${cwd}`);
}
// 2. Check environment health
const nodeVersion = await bash('node --version');
const npmVersion = await bash('npm --version');
console.log(` Node: ${nodeVersion}, npm: ${npmVersion}`);
// 3. Load previous session state
const progress = await loadProgressFile();
const gitLog = await bash('git log --oneline -10');
const featureList = await loadFeatureList();
console.log(` š Loaded progress from ${progress.lastSession}`);
console.log(` š Last commit: ${gitLog.split('\n')[0]}`);
// 4. Identify next task
const nextTask = featureList.features
.filter(f => !f.passes)
.sort((a, b) => a.priority - b.priority)[0];
if (!nextTask) {
console.log(' ā
All features complete!');
return { complete: true };
}
console.log(` šÆ Next task: ${nextTask.description}`);
// 5. Start dev server if needed
if (progress.devServerNeeded) {
await bash('npm run dev &');
await sleep(3000);
console.log(' š„ļø Dev server started');
}
return {
complete: false,
progress,
gitLog,
featureList,
currentTask: nextTask
};
}
State Persistence Strategies
Strategy 1: File-Based State
The simplest and most reliable approach:
// state/session-state.json
interface FileBasedState {
sessionId: string;
startedAt: string;
lastUpdated: string;
currentTask: {
featureId: string;
stepIndex: number;
status: 'in_progress' | 'testing' | 'blocked';
};
completedTasks: string[];
workingContext: {
lastFileEdited: string;
lastCommandRun: string;
openIssues: string[];
};
}
class FileStateManager {
private statePath = './state/session-state.json';
async save(state: FileBasedState): Promise<void> {
state.lastUpdated = new Date().toISOString();
await writeFile(this.statePath, JSON.stringify(state, null, 2));
}
async load(): Promise<FileBasedState | null> {
try {
const content = await readFile(this.statePath);
return JSON.parse(content);
} catch {
return null;
}
}
async update(patch: Partial<FileBasedState>): Promise<void> {
const current = await this.load();
await this.save({ ...current, ...patch });
}
}
Strategy 2: Progress File (Human-Readable)
Anthropic's approach uses a markdown progress file:
# claude-progress.txt
## Project: E-Commerce Store
Started: 2025-01-15
---
## Session 5 - 2025-01-17 14:30
### Completed
- [x] Shopping cart add item functionality
- [x] Cart item quantity update
- [x] Cart item removal
### In Progress
- [ ] Cart total calculation with tax
### Blocked
- Waiting for decision on tax API provider
### Notes
- Used React Context for cart state
- Consider using Zustand if state gets more complex
### Next Session Should
1. Resolve tax API decision
2. Complete cart total calculation
3. Start checkout flow UI
---
## Session 4 - 2025-01-16 10:00
[Previous session notes...]
Strategy 3: Git-Based State
Using git as the source of truth:
class GitStateManager {
async getCurrentState(): Promise<GitState> {
const branch = await bash('git branch --show-current');
const lastCommit = await bash('git log -1 --format="%H|%s|%ai"');
const [hash, message, date] = lastCommit.split('|');
const status = await bash('git status --porcelain');
const hasUncommitted = status.length > 0;
return {
branch,
lastCommit: { hash, message, date },
hasUncommittedChanges: hasUncommitted,
uncommittedFiles: status.split('\n').filter(Boolean)
};
}
async createCheckpoint(message: string): Promise<string> {
await bash('git add .');
await bash(`git commit -m "checkpoint: ${message}"`);
return await bash('git rev-parse HEAD');
}
async rollbackToCheckpoint(hash: string): Promise<void> {
await bash(`git reset --hard ${hash}`);
}
}
Handling Session Interruptions
Sessions can be interrupted unexpectedly. Plan for it:
interface InterruptionHandler {
// Graceful shutdown
onShutdownSignal(): Promise<void>;
// Crash recovery
onCrashRecovery(): Promise<RecoveryAction>;
// Timeout handling
onSessionTimeout(): Promise<void>;
}
class RobustSessionManager {
private checkpointInterval = 5 * 60 * 1000; // 5 minutes
private lastCheckpoint: Date;
async startSession() {
// Set up interrupt handlers
process.on('SIGINT', () => this.gracefulShutdown());
process.on('SIGTERM', () => this.gracefulShutdown());
// Check for crash recovery
const crashState = await this.detectCrashState();
if (crashState) {
await this.recoverFromCrash(crashState);
}
// Start periodic checkpoints
this.startCheckpointTimer();
}
private async gracefulShutdown() {
console.log('\nā ļø Shutdown signal received...');
// 1. Save current state
await this.saveCurrentState();
// 2. Commit any uncommitted work
const status = await bash('git status --porcelain');
if (status) {
await bash('git add .');
await bash('git commit -m "WIP: Session interrupted"');
}
// 3. Write handoff document
await this.writeHandoffDocument('interrupted');
console.log('ā
State saved. Safe to exit.');
process.exit(0);
}
private async detectCrashState(): Promise<CrashState | null> {
// Check for lock file (indicates unclean shutdown)
const lockExists = await fileExists('.session.lock');
if (!lockExists) return null;
// Load crash state
const state = await readFile('.session.lock');
return JSON.parse(state);
}
private async recoverFromCrash(crashState: CrashState) {
console.log('š§ Recovering from crash...');
// Check git state
const hasUncommitted = await bash('git status --porcelain');
if (hasUncommitted) {
// Option 1: Commit WIP
// Option 2: Stash changes
// Option 3: Discard changes
console.log(' Found uncommitted changes');
await bash('git stash -m "Crash recovery stash"');
}
// Restore to last known good state
if (crashState.lastGoodCommit) {
await bash(`git reset --hard ${crashState.lastGoodCommit}`);
}
// Clean up lock file
await deleteFile('.session.lock');
console.log('ā
Recovery complete');
}
private startCheckpointTimer() {
setInterval(async () => {
await this.createPeriodicCheckpoint();
}, this.checkpointInterval);
}
private async createPeriodicCheckpoint() {
const status = await bash('git status --porcelain');
if (status) {
await bash('git add .');
await bash('git commit -m "auto-checkpoint"');
console.log('šø Auto-checkpoint created');
}
}
}
The Handoff Document
At session end, create a clear handoff:
interface HandoffDocument {
// Session metadata
sessionId: string;
endedAt: string;
endReason: 'complete' | 'interrupted' | 'timeout' | 'error';
// Work status
taskAttempted: string;
taskStatus: 'completed' | 'in_progress' | 'blocked' | 'failed';
// For in-progress work
currentStep?: number;
totalSteps?: number;
lastActionTaken?: string;
// For blocked work
blockedReason?: string;
blockedNeedsHuman?: boolean;
// For next session
recommendedNextSteps: string[];
warnings: string[];
// Technical state
gitCommitHash: string;
devServerWasRunning: boolean;
}
async function createHandoffDocument(
session: SessionContext,
endReason: string
): Promise<HandoffDocument> {
const gitHash = await bash('git rev-parse HEAD');
const handoff: HandoffDocument = {
sessionId: session.id,
endedAt: new Date().toISOString(),
endReason,
taskAttempted: session.currentTask?.description || 'None',
taskStatus: session.taskStatus,
gitCommitHash: gitHash,
devServerWasRunning: session.devServerRunning,
recommendedNextSteps: [],
warnings: []
};
// Add context-specific recommendations
if (session.taskStatus === 'in_progress') {
handoff.currentStep = session.currentStepIndex;
handoff.totalSteps = session.totalSteps;
handoff.lastActionTaken = session.lastAction;
handoff.recommendedNextSteps.push(
`Continue from step ${session.currentStepIndex + 1}: ${session.nextStep}`
);
}
if (session.taskStatus === 'blocked') {
handoff.blockedReason = session.blockReason;
handoff.blockedNeedsHuman = true;
handoff.recommendedNextSteps.push(
`Resolve blocker: ${session.blockReason}`
);
}
// Check for warnings
const uncommitted = await bash('git status --porcelain');
if (uncommitted) {
handoff.warnings.push('Uncommitted changes exist');
}
await writeFile('handoff.json', JSON.stringify(handoff, null, 2));
return handoff;
}
Session Timeout Handling
Long sessions need timeout management:
class SessionTimeoutManager {
private maxSessionDuration = 4 * 60 * 60 * 1000; // 4 hours
private warningThreshold = 30 * 60 * 1000; // 30 min before timeout
private sessionStart: Date;
async startSession() {
this.sessionStart = new Date();
// Set warning timer
setTimeout(() => {
this.onTimeoutWarning();
}, this.maxSessionDuration - this.warningThreshold);
// Set hard timeout
setTimeout(() => {
this.onSessionTimeout();
}, this.maxSessionDuration);
}
private onTimeoutWarning() {
console.log('ā ļø Session timeout in 30 minutes');
console.log(' Recommend: Complete current task and checkpoint');
}
private async onSessionTimeout() {
console.log('ā° Session timeout reached');
// Force save state
await this.emergencySaveState();
// Create handoff
await createHandoffDocument(this.session, 'timeout');
process.exit(0);
}
getTimeRemaining(): number {
const elapsed = Date.now() - this.sessionStart.getTime();
return this.maxSessionDuration - elapsed;
}
}
E-Commerce Example: Complete Session Flow
async function ecommerceSession() {
const stateManager = new FileStateManager();
const sessionManager = new RobustSessionManager();
const timeoutManager = new SessionTimeoutManager();
// 1. STARTUP
await sessionManager.startSession();
await timeoutManager.startSession();
const context = await sessionStartup('ecommerce-store');
if (context.complete) {
console.log('š Project complete!');
return;
}
// 2. ACTIVE - Main work loop
try {
while (timeoutManager.getTimeRemaining() > 30 * 60 * 1000) {
const task = context.currentTask;
// Work on task
await implementTask(task);
// Update state
await stateManager.update({
currentTask: {
featureId: task.id,
stepIndex: task.currentStep,
status: 'in_progress'
}
});
// Check if task complete
const testResult = await runTests(task);
if (testResult.passed) {
task.passes = true;
await stateManager.update({
completedTasks: [...context.completedTasks, task.id]
});
// Get next task
context.currentTask = getNextTask(context.featureList);
if (!context.currentTask) {
console.log('š All tasks complete!');
break;
}
}
}
} finally {
// 3. SHUTDOWN - Always runs
await createHandoffDocument(context, 'complete');
console.log('ā
Session ended cleanly');
}
}
Key Takeaways
- Sessions have three phases - Startup, Active, Shutdown
- Startup must recover context - Load state, verify environment, identify next task
- Multiple persistence strategies - Files, progress docs, git
- Plan for interruptions - Graceful shutdown, crash recovery
- Handoff documents bridge sessions - Clear instructions for next session
- Timeouts prevent runaway sessions - Warn and checkpoint before timeout
Practice Exercise
Implement a session manager for a documentation generator:
- Design the session state structure
- Implement startup with crash detection
- Create a progress file format
- Write the handoff document generator
- Add timeout handling
Next Lesson
Now let's explore File-Based Agent Communication - how multiple agents coordinate through structured files rather than direct API calls.