📚 Docs / Workspace Management & Auto-Ingestion

Workspace Management & Auto-Ingestion

Workspace management in Lawyer Assistant provides per-project isolation with automatic document indexing. When you select a folder, it becomes your active workspace — everything the app saves lives in a dedicated workspace/ sub-folder, and a background watcher auto-ingests new documents you drop in.

1. What is a workspace?

A workspace is a user-picked project folder that becomes the active context for all operations:

Before workspace system (legacy)

Everything lived in a static data/ directory — one index for all documents, mixed chat history, no project separation.

After workspace system (current)

Each folder is self-contained. Point the app at a different folder and you get that folder's index, history, and settings — no cross-contamination.


2. Selecting a workspace

In the desktop app

Click "Work in a folder" in the sidebar → a native Electron directory dialog opens → pick your project folder. The backend receives the absolute path (no file re-upload needed).

Via API

bash# Set workspace
curl -X POST http://localhost:8765/api/workspace \
  -H "Content-Type: application/json" \
  -d '{"path": "/absolute/path/to/project"}'

# Get current workspace
curl http://localhost:8765/api/workspace

# Clear workspace (reset to no active workspace)
curl -X DELETE http://localhost:8765/api/workspace

Requirements

A workspace is mandatory for:

These endpoints return "No workspace selected. Select a project folder first." when none is active — there is no default data/ folder fallback.


3. Workspace storage layout

When a workspace is active, storage resolves as follows:

/path/to/your-project/
├── contract_A.pdf            ← raw uploads land here (visible as project docs)
├── agreement_B.docx          ← visible to the user
├── memo.txt
└── workspace/                ← private data folder (created automatically)
    ├── chroma_db/            ← vector index (per project)
    ├── bm25_index/           ← sparse index
    ├── processed/            ← temp artifacts (Docling images, etc.)
    └── history.json          ← chat history + working set + GPU profile

No workspace active → falls back to project data/ directory:

project-root/
└── data/
    ├── raw/
    ├── chroma_db/
    ├── bm25_index/
    └── processed/

Workspace override is runtime-only (resets on server restart or DELETE /api/workspace).


4. Background folder watcher (auto-ingestion)

The watcher is a daemon thread that polls the active workspace folder for new documents and auto-ingests them — drop a contract into the folder and it becomes searchable within seconds, with zero user action.

How it works

┌────────────────────────────────────────────────────────────┐
│                   FOLDER WATCHER LOOP                       │
└────────────────────────────────────────────────────────────┘

1. Every 8 seconds (configurable): compute folder fingerprint
   ├─ {relative_path: (size, mtime_ns)} for supported documents
   ├─ Excludes workspace/ storage dirs
   ├─ Excludes temp/lock files (~$*, .~lock*, *.tmp, *.bak, ...)
   └─ Cheap (no ChromaDB call)

2. Compare fingerprint to last scan:
   ├─ No change → skip cycle (idle, no disk/GPU work)
   └─ Changed → proceed to step 3

3. Acquire INGEST_LOCK (non-blocking):
   ├─ Manual SSE ingest in flight? → skip cycle, retry next poll
   └─ Lock acquired → proceed to step 4

4. Run stream_ingest_directory():
   ├─ Docling parses new files
   ├─ StructureAwareChunker splits them
   ├─ BGE-M3 embeds chunks
   ├─ ChromaDB stores vectors
   └─ BM25 index rebuilt

5. Commit fingerprint → next cycle (8s later)

Key features

Polling, not fs events

Cheap skip detection

Serialization with manual ingestion

Self-stopping on workspace switch

Status exposed to UI


5. Watcher status API

bashcurl http://localhost:8765/api/workspace/watch/status

Returns:

json{
  "watch": {
    "watching": true,
    "root": "/absolute/path/to/project",
    "last_scan_at": 1750000000.0,
    "last_scan_duration_ms": 45,
    "files_found": ["contract.pdf", "agreement.docx"],
    "files_indexed": ["contract.pdf"],
    "chunks_indexed": 23,
    "skipped": 1,
    "last_error": null
  }
}

Fields:


6. Manual workspace ingestion

Trigger a one-time ingestion of every supported document in the workspace:

Non-streaming (blocking)

bashcurl -X POST http://localhost:8765/api/workspace/ingest

Returns when done:

json{
  "chunks": 458,
  "path": "/absolute/path/to/project",
  "elapsed_ms": 12340
}

Streaming (SSE with progress)

bashcurl -X POST http://localhost:8765/api/workspace/ingest/stream

Streams events:

event: ingest_start
data: {"type":"ingest_start","files":["a.pdf","b.docx"],"total":2,"skipped":0}

event: ingest_progress
data: {"type":"ingest_progress","file":"a.pdf","current":1,"total":2,"chunks":23}

event: ingest_progress
data: {"type":"ingest_progress","file":"b.docx","current":2,"total":2,"chunks":45}

event: ingest_done
data: {"type":"ingest_done","chunks":68,"skipped":0,"elapsed_ms":3456}

Force re-index

Add ?force=1 to re-index already-indexed files (rebuilds the entire index):

bashcurl -X POST "http://localhost:8765/api/workspace/ingest/stream?force=1"

Cancel streaming ingestion

bashcurl -X POST http://localhost:8765/api/workspace/ingest/cancel

Stops cleanly at the next file boundary (already-completed files stay indexed).


7. Workspace file listing

Get discovered documents in the active workspace (UI "workspace panel"):

bashcurl http://localhost:8765/api/workspace

Returns:

json{
  "active": true,
  "path": "/absolute/path/to/project",
  "name": "project",
  "workspace_dir": "/absolute/path/to/project/workspace",
  "gpu_profile": { "residency_mode": "adaptive", ... },
  "files": [
    { "name": "contract_A.pdf", "size": 123456, "modified": 1750000000.0 },
    { "name": "agreement_B.docx", "size": 67890, "modified": 1750000100.0 }
  ]
}

Files are:


8. Configuration

All watcher configuration is in backend/legal_retrieval/workspace_watcher.py:

ConstantDefaultMeaning
DEFAULT_POLL_INTERVAL8.0Seconds between folder scans
INGEST_LOCK(threading.Lock)Serializes all ingestion (watcher + manual)

Note: The poll interval is hardcoded, not an env var. To change it, pass poll_interval=<seconds> when constructing WorkspaceWatcher() (or modify DEFAULT_POLL_INTERVAL directly).


9. Per-project GPU profiles

Each workspace carries its own GPU residency profile (mode, hot window, max resident, ...) persisted in workspace/history.json under "gpu_profile".

bash# Get active project's GPU profile
curl http://localhost:8765/api/gpu/profile

# Apply and persist a profile to the active project
curl -X POST http://localhost:8765/api/gpu/profile \
  -H "Content-Type: application/json" \
  -d '{"residency_mode":"persistent","max_resident":3}'

When you switch projects (POST /api/workspace), the new project's GPU profile is automatically loaded — settings never leak between projects.

See GPU_MANAGEMENT.md §7 for GPU profile details.


10. Workspace history (chat + working set + GPU)

workspace/history.json stores:

The frontend auto-saves this file via POST /api/workspace/history:

bashcurl -X POST http://localhost:8765/api/workspace/history \
  -H "Content-Type: application/json" \
  -d @history.json

Read it back:

bashcurl http://localhost:8765/api/workspace/history

Returns:

json{
  "history": {
    "conversations": [ ... ],
    "gpu_profile": { ... }
  }
}

Schema:

Frontend keys (freebuff-chat-storage, freebuff-working-set) are scoped per project via a hash suffix — switching workspaces in the UI re-points the localStorage keys, and the server-side history.json acts as the durable backup.


11. Implementation details

FileResponsibility
backend/legal_retrieval/workspace_watcher.pyWatcher thread, fingerprinting, auto-ingestion
backend/legal_retrieval/config.pyWorkspace root storage, path resolution, GPU profile persistence
backend/main.py/api/workspace endpoints, watcher start/stop
backend/tests/test_workspace.pyWatcher tests (fingerprint, auto-ingest, lock coordination)

Watcher lifecycle

Thread safety


12. Troubleshooting

SymptomCauseFix
Watcher not runningNo workspace activeSelect a folder via /api/workspace
New files not indexedWatcher stopped or erroredCheck /api/workspace/watch/statuslast_error
Files indexed twiceManual ingest + watcher both ranThis is safe — stream_ingest_directory skips already-indexed files
Watcher slowLarge corpus, small GPULower embedding_batch_size or use CPU; watcher respects GPU manager tuning
Fingerprint false-positiveFile mtime changed but content sameHarmless — stream_ingest_directory skips re-indexing

13. Why polling, not fs events?

ApproachProsCons
Polling (current)✅ No new dependency<br>✅ Works on network drives<br>✅ Cross-platform (no platform-specific quirks)<br>✅ Trivially safe to restart⚠️ 8-second latency (tunable)
fs events (watchdog/chokidar)✅ Instant reaction❌ New dependency<br>❌ Platform-specific edge cases<br>❌ Network drive issues<br>❌ Missed events on restart

For a desktop app with human-paced workflows (dropping contracts), 8-second latency is imperceptible — the simplicity/reliability trade-off favors polling.


14. Future enhancements

Planned but not yet implemented:

See PIPELINE_ROADMAP.md for other planned features.


DocWhat's covered
BACKEND.md §2Workspace API endpoints
ARCHITECTURE.mdSystem overview, storage layout
GPU_MANAGEMENT.md §7Per-project GPU profiles
DATA_FLOW.mdIngestion pipeline deep dive
TROUBLESHOOTING.md §4Workspace-related failures

Questions, answered

Short, self-contained answers about this guide.

Why do I need a workspace?

A project folder is required — there is no default fallback. It scopes retrieval to the right corpus, keeps per-project indexes and history separate, and prevents results bleeding between cases.

How do I set up a workspace?

Click 'Work in a folder' in the app and pick your project directory. Then run 'Ingest all files' (or attach files individually) to index everything; uploads land in the active workspace.

What lives in the workspace?

Raw uploads, processed chunks, the vector + BM25 indexes, and project-scoped history. Switching folders switches the whole working set, and folder-specific project history is saved.