šŸ”„ 0
⭐ 0
Lesson 5 of 10 25 min +200 XP

Session Management & State Persistence

The Session Lifecycle

Every agent session follows a predictable lifecycle:

Session 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.