Multi-Workspace Management
Manage multiple projects with hot-swapping, file watching, backup systems, and intelligent cleanup.
Nella can manage up to 50 workspaces simultaneously — each with its own index, configuration, and context. Switch between projects instantly with LRU caching, and let the registry handle cleanup, backups, and migrations automatically.
Architecture
WorkspaceRegistry (singleton)
├── WorkspaceEntry[] # Persisted to ~/.nella/workspaces.json
├── RegistryBackupManager # Rolling JSON backups before every write
├── RegistryMigrationManager # Semver schema migrations (1.1.0 → 2.0.0)
├── WorkspaceValidator # 8 error codes + 5 warning codes
└── FileLock # Cross-process O_EXCL locking
WorkspaceSwitcher (singleton)
├── LRUCache<Workspace> # 3-slot cache with 30-min TTL
└── hot-swap between workspaces without reloading
Core Components
WorkspaceRegistry
The global registry manages workspace CRUD operations, persisting entries to ~/.nella/workspaces.json with file locking and backup-before-write safety.
import { getWorkspaceRegistry } from '@usenella/core';
const registry = getWorkspaceRegistry();
// Register a new workspace
const entry = await registry.register('/path/to/project', {
name: 'my-api',
config: {
autoIndex: true,
indexOnChange: true,
include: ['src/**', 'lib/**'],
exclude: ['node_modules/**', 'dist/**'],
},
});
// List all workspaces
const workspaces = await registry.list();
// Remove a workspace
await registry.remove(entry.id);
Settings:
| Setting | Default | Description |
|---|---|---|
maxWorkspaces | 50 | Maximum registered workspaces |
autoCleanup | true | Evict stale workspaces automatically |
cleanupAfterDays | 30 | Days of inactivity before cleanup |
WorkspaceSwitcher
Hot-swap between workspaces with an LRU cache that keeps recently-used workspaces in memory. Eviction calls workspace.dispose() to free resources.
import { getWorkspaceSwitcher } from '@usenella/core';
const switcher = getWorkspaceSwitcher();
// Switch to a workspace (cached if recently used)
const workspace = await switcher.switchTo('workspace-id');
// Get current active workspace
const active = switcher.getActive();
Cache Configuration:
| Option | Default | Description |
|---|---|---|
cacheSize | 3 | Number of workspaces kept in memory |
cacheTtl | 30 min | Time before cached workspace is evicted |
FileWatcher
Recursive directory watcher with debounced batch processing and glob-based filtering. Uses fs.watch() with minimatch for pattern matching.
const watcher = workspace.startWatching();
// File changes are batched (1s debounce) and trigger incremental re-indexing
| Option | Default | Description |
|---|---|---|
debounceMs | 1000 | Milliseconds to batch file changes |
include | ['**/*'] | Glob patterns to watch |
exclude | ['node_modules/**'] | Glob patterns to ignore |
FileLock
Cross-process file locking using atomic O_EXCL flag creation. Prevents concurrent registry writes from multiple Nella instances.
- Lock timeout: 5,000 ms
- Stale detection: 30,000 ms — checks lock age +
process.kill(pid, 0)liveness - Fallback: Synchronous acquire if async fails
Workspace Configuration
Each workspace has its own configuration for indexing, embedding, and search:
interface WorkspaceConfig {
autoIndex: boolean; // Auto-index on registration
indexOnChange: boolean; // Re-index on file changes
include: string[]; // File glob patterns to include
exclude: string[]; // File glob patterns to exclude
embedder: {
provider: 'voyage' | 'openai' | 'local'; // Default: 'voyage'
model: string; // Default: 'voyage-code-2'
dimensions: number; // Default: 1536
};
chunking: {
maxTokens: number; // Default: 512
overlap: number; // Default: 50
strategy: 'ast' | 'recursive' | 'fixed'; // Default: 'ast'
};
search: {
vectorWeight: number; // Default: 0.4
lexicalWeight: number; // Default: 0.6
rerankEnabled: boolean;
topK: number; // Default: 10
};
}
Workspace Entry
Every registered workspace is tracked with metadata:
interface WorkspaceEntry {
id: string; // SHA-256(path)[0:8] + base36(timestamp)
name: string;
path: string;
createdAt: string;
lastAccessed: string;
indexStatus: 'ready' | 'indexing' | 'stale' | 'none' | 'error';
stats: {
filesIndexed: number;
chunksCount: number;
totalTokens: number;
};
config?: WorkspaceConfig;
}
Validation
The WorkspaceValidator runs 13 checks on workspace entries:
Errors (8 codes):
- Path does not exist
- Path is not accessible (permissions)
- Path is not a directory
- Duplicate workspace path
- Registry file corrupted
- Schema version mismatch
- Lock acquisition timeout
- Maximum workspaces exceeded
Warnings (5 codes):
- No
.gitdirectory found - Index stale (> 7 days since last indexing)
- Large workspace (> 50,000 files)
- Long unused (> 30 days since last access)
- Missing configuration defaults
Backup & Migration
Rolling Backups
Every registry write creates a backup first. Up to 5 rolling backups are maintained in ~/.nella/backups/.
Schema Migrations
The registry schema evolves across versions. Migrations run automatically on load:
| Version | Changes |
|---|---|
1.1.0 | Added validation fields |
1.2.0 | Added sync settings |
1.3.0 | Added tags and metadata |
2.0.0 | Sync adapter support (syncTier, syncId, lastSyncedAt) |
Events
Workspace operations emit events for real-time monitoring:
| Event | Description |
|---|---|
created | New workspace registered |
updated | Workspace configuration changed |
removed | Workspace unregistered |
switched | Active workspace changed |
index:start | Indexing started |
index:complete | Indexing finished |
index:error | Indexing failed |
watch:start | File watcher started |
watch:stop | File watcher stopped |
files:changed | File changes detected |
Import / Export
Workspaces can be exported and imported for team sharing or machine migration:
// Export all workspaces
const data = await registry.export();
// Import on another machine
await registry.import(data, { mode: 'merge' }); // or 'overwrite'
Tip
Use nella connect to auto-configure MCP clients after importing workspaces on a new machine.