HANDOFF S138.6 Generation Process Persistence

Documentation for Handoffs

HANDOFF S138.6 — Generation Process Persistence & Recovery

Date: 2026-02-23 Session: 138.6 Type: Architecture / Feature Priority: HIGH — Every page refresh or tab crash loses 8-10 minutes of generation work


Problem Statement

When a user starts platform generation (an 8-10 minute process), any interruption destroys everything:

There is no way to come back to a running or completed generation. Each visit to /vibe starts completely fresh.


Surprising Finding: Backend Is Already 80% Ready

Investigation reveals the backend already has reconnection, resumption, and checkpoint infrastructure — it's just never wired up on the frontend.

What Already Exists (Backend)

Feature Endpoint / Code Status
Session creation modgen-${UUID.substring(0,8)} ✅ Works
SSE reconnection GET /stream/:sessionId ✅ Built, never called
Phase resumption POST /resume/:sessionId ✅ Built (Session 137), never called
Session status check GET /status/:sessionId ✅ Built, never called
Checkpoint load GET /checkpoint/:sessionId ✅ Built, never called
MongoDB persistence generation_checkpoints collection ✅ Saves per-phase results
Resumable session finder findResumableSessions(userId) ✅ Built, never called
ReplaySubject buffer Last 50 SSE events per session ✅ Late-join gets history

What Already Exists (Frontend)

Feature Location Status
resumeWorkflow(sessionId) WorkflowStateService (lines 313-365) ✅ Built, never called
workflowId signal WorkflowStateService ✅ Stores sessionId
restoreGenerationState() VibeUniverseGeneratorComponent (lines 786-837) ⚠️ Uses sessionStorage only, no backend check

What's Missing

Gap What Needs To Happen
Route parameter Add :processId to vibe route
URL persistence Push sessionId to URL when generation starts
Init check On component init, check route for processId → reconnect
Generation history UI to browse past/active generations
Backend session → MongoDB link In-memory sessions lost on restart; rely on checkpoints

Implementation Plan

Phase 1: Wire the ProcessID into the Route (Priority: HIGH)

File: vibe-universe.routes.ts

Current:

export const VibeUniverseRoutes: Routes = [
  { path: '', loadComponent: () => import('./components/vibe-universe-generator/...') }
];

Change to:

export const VibeUniverseRoutes: Routes = [
  { path: '', loadComponent: () => import('./components/vibe-universe-generator/...') },
  { path: 'process/:processId', loadComponent: () => import('./components/vibe-universe-generator/...'),
    data: { tool: 'vibe', aiEnabled: true, fullScreen: true, resumeMode: true }
  }
];

This gives us two routes:


Phase 2: Capture & Persist the ProcessID (Priority: HIGH)

File: vibe-universe-generator.component.ts

When generation starts and the first SSE event arrives with a sessionId:

// In the SSE event handler, after receiving the first event with sessionId:
private onSessionIdReceived(sessionId: string): void {
  // 1. Update the URL without triggering navigation
  this.router.navigate(
    ['process', sessionId],
    { relativeTo: this.route, replaceUrl: true }
  );

  // 2. Also persist to localStorage as backup
  localStorage.setItem(`vibe-generation:${this.universeId}:active`, JSON.stringify({
    sessionId,
    startedAt: Date.now(),
    description: this.platformDescription(),
    universeId: this.universeId,
    spaceId: this.spaceId
  }));
}

File: workflow-state.service.ts

The sessionId is already extracted from SSE events but only stored in a signal. Add an output event:

// In startWorkflow(), when first SSE event arrives:
this.eventSource.addEventListener('module-gen.started', (e: MessageEvent) => {
  const raw = JSON.parse(e.data);
  const sessionId = raw.sessionId || raw.data?.sessionId;
  if (sessionId) {
    this.workflowId.set(sessionId);
    this.sessionIdReceived.emit(sessionId);  // New: notify component
  }
});

Phase 3: Reconnect on Page Load (Priority: HIGH)

File: vibe-universe-generator.component.ts

Update ngOnInit to check for processId in route:

ngOnInit(): void {
  const processId = this.route.snapshot.params['processId'];

  if (processId) {
    // Resume mode: reconnect to existing generation
    this.reconnectToGeneration(processId);
  } else {
    // Check localStorage for recent active generation in this universe
    const active = this.getActiveGeneration();
    if (active && active.sessionId && (Date.now() - active.startedAt) < 3600000) {
      // Prompt user: "You have an active generation. Resume or start fresh?"
      this.showResumePrompt(active);
    } else {
      // Fresh start: show template picker
      this.mode.set('idle');
    }
  }
}

private async reconnectToGeneration(sessionId: string): Promise<void> {
  this.mode.set('generating');

  // Step 1: Check backend status first
  try {
    const status = await firstValueFrom(
      this.http.get<any>(`${this.vittoApiUrl}/module-generation/status/${sessionId}`)
    );

    if (status.status === 'completed') {
      // Generation already finished — restore final state
      this.restoreCompletedState(status);
      return;
    }

    if (status.status === 'failed' || status.status === 'interrupted') {
      // Offer to resume from checkpoint
      this.showResumeFromCheckpoint(sessionId, status);
      return;
    }

    // Still running — reconnect to SSE stream
    this.workflowState.resumeWorkflow(sessionId).subscribe({
      next: (event) => this.handleWorkflowEvent(event),
      error: (err) => this.handleReconnectError(err, sessionId)
    });

  } catch (err) {
    // Session not found in memory — try checkpoint
    this.attemptCheckpointResume(sessionId);
  }
}

Phase 4: Backend — Ensure Checkpoint Saves SessionId Early (Priority: MEDIUM)

File: module-gen-phase.executor.ts

The checkpoint is already created at generation start. Verify it saves immediately (before any LLM call), so even if the first phase crashes, the sessionId is in MongoDB:

// In orchestrateGeneration():
const session = this.sessionManager.createSession(request);
const sessionId = session.id;

// Checkpoint IMMEDIATELY (before ENHANCE phase)
await this.checkpointService.createCheckpoint({
  sessionId,
  request: { description: request.description, spaceId: request.spaceId, universeId: request.universeId },
  status: 'in_progress',
  userId: request.userId,
  spaceId: request.spaceId,
  universeId: request.universeId,
});

// Now proceed with ENHANCE...

Also ensure the module-gen.started SSE event fires with sessionId BEFORE the first phase begins, so the frontend can capture it.


Phase 5: Generation History UI (Priority: MEDIUM)

Add a small "history" section to the vibe page that shows past generations for this universe:

Backend endpoint (already partially exists via findResumableSessions):

GET /vitto-api/module-generation/history?universeId=xxx&userId=yyy&limit=10

Returns:

[
  {
    "sessionId": "modgen-a1b2c3d4",
    "description": "Fitness studio platform with membership management...",
    "status": "completed",
    "completedPhases": ["enhance", "analyze", "decompose", "plan", "execute", "integrate", "deploy"],
    "modulesGenerated": 11,
    "startedAt": "2026-02-23T14:27:54Z",
    "completedAt": "2026-02-23T14:38:33Z",
    "duration": "10m 39s"
  }
]

Frontend: Show as a collapsible panel or sidebar on the vibe page:

Recent Generations
─────────────────────
✅ Fitness Studio (11 modules) — 10 min ago
   └ Click to load result
❌ E-Commerce Platform (failed at EXECUTE) — 2 hours ago
   └ Click to retry from checkpoint
🔄 Restaurant App (in progress, PLAN phase) — just now
   └ Click to reconnect

Phase 6: Multi-Tab Safety (Priority: LOW)

With processId in the URL, add a guard:

// Before starting a new generation, check if one is already running
private async checkForActiveGeneration(): Promise<boolean> {
  const active = await firstValueFrom(
    this.http.get<any>(`${this.vittoApiUrl}/module-generation/active?universeId=${this.universeId}`)
  );
  if (active?.sessionId) {
    this.toast.warning('A generation is already running. Redirecting...');
    this.router.navigate(['vibe', 'process', active.sessionId], { relativeTo: this.universeRoute });
    return false; // Don't start new generation
  }
  return true;
}

File Change Summary

File Changes
vibe-universe.routes.ts Add process/:processId child route
vibe-universe-generator.component.ts Add ngOnInit processId check, reconnectToGeneration(), onSessionIdReceived() URL update, resume prompt UI, generation history panel
workflow-state.service.ts Add sessionIdReceived output/callback, ensure resumeWorkflow() is working correctly
module-generation.controller.ts Add /history endpoint, verify /stream/:sessionId returns proper state for late-joins
module-gen-phase.executor.ts Ensure checkpoint saves before first phase, emit module-gen.started SSE event with sessionId early
checkpoint.service.ts Add findByUniverse() query for history endpoint

Key Design Decisions

  1. ProcessId = SessionId — Reuse the existing modgen-{uuid} format. No need for a separate concept.

  2. URL is the source of truth — /vibe/process/modgen-a1b2c3d4 makes the generation bookmarkable, shareable, and survives refresh. localStorage is backup only.

  3. Backend checkpoint is the recovery mechanism — In-memory sessions may be lost on restart, but MongoDB checkpoints persist for 7 days. The resume endpoint rebuilds from checkpoint.

  4. Graceful degradation — If processId in URL points to a dead session with no checkpoint, show a friendly "This generation has expired" message with option to start fresh.

  5. No breaking changes — The existing /vibe route still works as a fresh start. The new /vibe/process/:processId is additive.


Testing Checklist