# SPRINT-GAP-002: State Synchronization Gaps

| Attribute | Value |
|-----------|-------|
| **Sprint** | GAP-002 |
| **Size** | M (Medium) |
| **Owner** | TBD |
| **Phase** | Hardening |
| **Prerequisites** | None |
| **Status** | ✅ COMPLETE |
| **Completed** | 2026-01-02 |

## Completion Summary

All state synchronization gaps have been addressed:

- **Meeting cache invalidation**: TTL-based staleness (30s) with automatic refresh on reconnect
- **State sync on reconnect**: Callback system for refreshing critical state after connection restored
- **Sync run expiry metadata**: Backend responses include expiry information
- **Integration validation errors**: Surfaced to user via error events
- **Cache version header**: Server state version for cache invalidation

## Resolved Issues

- [x] Determine cache invalidation strategy (push vs poll vs hybrid) → **TTL + reconnect refresh**
- [x] Define acceptable staleness window for meeting data → **30 seconds**
- [x] Decide on WebSocket/SSE vs polling for real-time updates → **Polling with reconnect sync**

## Validation Status

| Component | Status | Resolution |
|-----------|--------|------------|
| Meeting cache | ✅ Fixed | TTL-based invalidation (30s staleness), refresh on reconnect |
| Sync run cache (backend) | ✅ Fixed | Expiry metadata added to responses |
| Active project resolution | ✅ Verified | Client uses `Meeting.project_id` from responses |
| Integration ID validation | ✅ Fixed | Errors surfaced via `integration:validation-error` events |

## Objective

Ensure consistent state between backend and client by implementing proper cache invalidation, explicit state communication, and recovery mechanisms for stale data.

## Key Decisions

| Decision | Choice | Rationale |
|----------|--------|-----------|
| Cache invalidation | TTL-based + reconnect refresh | Simple, predictable, no WebSocket dependency |
| Staleness window | 30 seconds | Balance freshness vs server load |
| Active project sync | Client consumption of response | Meeting already includes project_id |
| Sync run status | Fixed-interval polling with expiry | Current hook polls at constant interval |

## Implementation Details

### 1. Meeting Cache Invalidation (✅ Complete)

**Files Modified:**
- `client/src/lib/cache/meeting-cache.ts` - Added TTL tracking and staleness checks
- `client/src/api/reconnection.ts` - Added `onReconnect` callback for cache refresh

**Implementation:**
```typescript
// STALENESS_THRESHOLD_MS = 30_000 (30 seconds)
isStale(meetingId: string): boolean {
  const entry = this.cache.get(meetingId);
  if (!entry) return true;
  return Date.now() - entry.cachedAt > STALENESS_THRESHOLD_MS;
}

invalidateAll(): void {
  this.cache.clear();
}
```

### 2. State Sync on Reconnect (✅ Complete)

**Files Modified:**
- `client/src/api/reconnection.ts` - Added callback registration and invocation

**Implementation:**
```typescript
const reconnectCallbacks: Array<() => void | Promise<void>> = [];

export function onReconnect(callback: () => void | Promise<void>): () => void {
  reconnectCallbacks.push(callback);
  return () => { /* unregister */ };
}

// Called after successful reconnection
await Promise.all(reconnectCallbacks.map(cb => Promise.resolve(cb())));
```

### 3. Sync Run Expiry Metadata (✅ Complete)

**Files Modified:**
- `src/noteflow/grpc/proto/noteflow.proto` - Added `expires_at` field
- `src/noteflow/grpc/_mixins/sync.py` - Populate expiry in responses
- `src/noteflow/grpc/_mixins/converters/_external.py` - Convert expiry to proto

**Implementation:**
```protobuf
message SyncRun {
  // ... existing fields
  optional string expires_at = 10;  // ISO 8601 timestamp
}
```

### 4. Integration Validation Errors (✅ Complete)

**Files Modified:**
- `client/src/lib/preferences.ts` - Emit error events instead of console.warn
- `client/src/hooks/use-integration-sync.ts` - Listen for validation errors

**Implementation:**
```typescript
validateCachedIntegrations().catch((err: unknown) => {
  window.dispatchEvent(new CustomEvent('integration:validation-error', {
    detail: { error: err instanceof Error ? err.message : 'Validation failed' }
  }));
});
```

### 5. Cache Version Header (✅ Complete)

**Files Modified:**
- `src/noteflow/grpc/proto/noteflow.proto` - Added `state_version` to ServerInfo
- `src/noteflow/grpc/_mixins/protocols.py` - Added `get_state_version()` method
- `src/noteflow/grpc/service.py` - Implement state version tracking
- `client/src/api/tauri-adapter.ts` - Check version on connect, invalidate on mismatch

**Implementation:**
```protobuf
message ServerInfo {
  // ... existing fields
  int64 state_version = 10;  // Increment on breaking state changes
}
```

## Files Modified

### Backend
- [x] `src/noteflow/grpc/proto/noteflow.proto` - Added `expires_at`, `state_version`
- [x] `src/noteflow/grpc/_mixins/sync.py` - Expiry metadata in responses
- [x] `src/noteflow/grpc/_mixins/protocols.py` - `get_state_version()`, `increment_state_version()`
- [x] `src/noteflow/grpc/service.py` - State version tracking
- [x] `src/noteflow/grpc/_mixins/converters/_external.py` - Expiry conversion

### Client
- [x] `client/src/lib/cache/meeting-cache.ts` - TTL-based invalidation
- [x] `client/src/api/reconnection.ts` - `onReconnect` callback system
- [x] `client/src/api/tauri-adapter.ts` - State version checking
- [x] `client/src/lib/preferences.ts` - Validation error events
- [x] `client/src/hooks/use-integration-sync.ts` - Error event handling

## Tests Added

- [x] `test_ttl_based_cache_invalidation` - Verifies staleness detection after threshold
- [x] `test_cache_invalidated_after_reconnection` - Verifies `invalidateAll()` called on reconnect
- [x] `test_on_reconnect_callback_invoked` - Verifies callback system works
- [x] `test_validate_integration_surfaces_error` - Verifies error events emitted
- [x] `test_state_version_triggers_invalidation` - Verifies version mismatch handling

## Quality Gates

- [x] No stale meeting states shown after reconnection
- [x] Sync run expiry handled gracefully
- [x] Active project always known to client
- [x] Integration validation errors surface to user
- [x] All cache operations have invalidation path
- [x] All quality checks pass (`make quality`)

## Test Strategy

### Fixtures
- Mock server with controllable state version
- Multi-client simulation
- Network partition simulation

### Test Cases

| Case | Input | Expected | Status |
|------|-------|----------|--------|
| Meeting modified by server | Create, modify via API, refresh | Client shows updated state | ✅ |
| Sync run expires | Start sync, prune run, check status | Graceful "expired" message | ✅ |
| Reconnection | Disconnect, modify, reconnect | State synchronized | ✅ |
| Active project | Create meeting without project_id | Response includes resolved project_id | ✅ |
| Cache version bump | Server restart with new version | Client invalidates caches | ✅ |

## Migration Notes

No breaking changes introduced. All new fields are optional:
- `SyncRun.expires_at` - Optional, only populated when expiry is known
- `ServerInfo.state_version` - Optional, defaults to 0 for backwards compatibility

Clients that don't handle these fields will continue to work as before.
