Skip to content

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:

SettingDefaultDescription
maxWorkspaces50Maximum registered workspaces
autoCleanuptrueEvict stale workspaces automatically
cleanupAfterDays30Days 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:

OptionDefaultDescription
cacheSize3Number of workspaces kept in memory
cacheTtl30 minTime 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
OptionDefaultDescription
debounceMs1000Milliseconds 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 .git directory 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:

VersionChanges
1.1.0Added validation fields
1.2.0Added sync settings
1.3.0Added tags and metadata
2.0.0Sync adapter support (syncTier, syncId, lastSyncedAt)

Events

Workspace operations emit events for real-time monitoring:

EventDescription
createdNew workspace registered
updatedWorkspace configuration changed
removedWorkspace unregistered
switchedActive workspace changed
index:startIndexing started
index:completeIndexing finished
index:errorIndexing failed
watch:startFile watcher started
watch:stopFile watcher stopped
files:changedFile 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.