# Fix: Workspace Sources and Artifacts Wiped on Each Page Load

**Date:** 2026-03-16  
**Branch:** `handlerww/fix-example`  
**Affected file:** `frontend/lib/workspace.tsx`

---

## Symptoms

- Users reported that after clicking **Share** on the Example workspace, the chat history appeared to be cleared.
- Separately, a workspace created 3 days prior lost its Jira issue sources and accumulated context on the next visit.
- The issue was intermittent — on fast networks it appeared to self-heal, but on slower connections or under light load it was reliably reproducible.

---

## Root Cause

`WorkspaceProvider` in `frontend/lib/workspace.tsx` initializes with `sources = []` and `artifacts = []`.  
The init effect fired three async operations but called `setInitialized(true)` **synchronously** before any of them completed:

```tsx
// BEFORE (broken)
fetchSources(workspaceId).then((raw) => {
  // ...setSources(correctData)  ← arrives ~200–500 ms later
});
fetchArtifacts(workspaceId).then((raw) => {
  // ...setArtifacts(correctData) ← arrives ~200–500 ms later
});

setInitialized(true);  // ← runs immediately, while sources/artifacts are still []
```

Two "persist" effects are keyed on `initialized`:

```tsx
useEffect(() => {
  if (!workspaceId || !initialized) return;
  saveSources(workspaceId, sources);   // fired instantly with sources = []
}, [sources, workspaceId, initialized]);

useEffect(() => {
  if (!workspaceId || !initialized) return;
  saveArtifacts(workspaceId, artifacts); // fired instantly with artifacts = []
}, [artifacts, workspaceId, initialized]);
```

The moment `initialized` flipped to `true`, React re-rendered with the still-empty state arrays, and both save effects issued `PUT /sources` and `PUT /artifacts` with empty payloads to the backend.

A second round of saves followed once the async fetches resolved with the real data — but because HTTP requests can complete out of order (especially HTTP/2 multiplexing), the **empty-payload PUT occasionally arrived at the server after the correct-data PUT**, permanently overwriting the database rows with `[]`.

### Why the Example workspace was worse

For a workspace with no saved sources/artifacts yet, the code performed a *second* async hop — fetching workspace metadata to check `is_example` before seeding defaults:

```tsx
fetchWithAuth(`${API}/api/workspaces/${workspaceId}`)
  .then(r => r.ok ? r.json() : null)
  .then(meta => {
    setSources(meta?.is_example ? createExampleSources() : createDefaultSources());
  });
```

This two-hop chain widened the race window significantly.  
On the next page load, `fetchSources` returned `[]`, the seeding branch fired again with `createExampleSources()` (or `createDefaultSources()`), and any user-added Jira issue sources accumulated over days were gone.

### Why "Clicking Share" seemed to trigger it

Clicking **Share** did not directly clear any data. But it caused the owner to re-evaluate workspace state right after mounting, which made the timing tighter — increasing the probability that the empty PUT raced past the correct-data PUT.

---

## Fix

**`frontend/lib/workspace.tsx` — init `WorkspaceProvider` as an `async` IIFE; call `setInitialized(true)` only after state is set.**

Key changes:

1. `Promise.all` to load sources and artifacts in parallel (single round-trip latency instead of two sequential fetches).
2. Fetch workspace meta only once, inside its own `try/catch` so a network failure there cannot cause `setInitialized` to fire before `setSources`/`setArtifacts`.
3. An `active` flag (`let active = true`) to drop stale results if the component unmounts while fetches are in-flight.
4. `setInitialized(true)` moved to **after** `setSources(...)` and `setArtifacts(...)`, so the persist effects first fire with real data.
5. The outer `catch` still calls `setInitialized(true)` so the UI doesn't hang on a hard error — but only reaches that path if `Promise.all(fetchSources, fetchArtifacts)` itself throws, not if the meta fetch fails.

```tsx
// AFTER (fixed)
let active = true;

(async () => {
  try {
    const [rawSources, rawArtifacts] = await Promise.all([
      fetchSources(workspaceId),
      fetchArtifacts(workspaceId),
    ]);
    if (!active) return;

    let meta: any = null;
    if (rawSources.length === 0 || rawArtifacts.length === 0) {
      try {
        const r = await fetchWithAuth(`${API}/api/workspaces/${encodeURIComponent(workspaceId)}`);
        if (!active) return;
        meta = r.ok ? await r.json() : null;
        if (!active) return;
      } catch {
        // meta stays null — fall back to default sources/artifacts below
      }
    }

    // setSources / setArtifacts with correct data ...

    setInitialized(true);  // ← only fires after state is populated
  } catch {
    if (active) setInitialized(true);
  }
})();

return () => { active = false; };
```

---

## Impact

- Sources and artifacts can no longer be wiped by a race between the seeding PUT and the real-data PUT.
- The workspace meta API is called at most once per mount (previously called up to twice — once for sources, once for artifacts — when both tables were empty).
- No changes to backend, API contract, or database schema.
